From 4df1fee813e33c1bf4143c35791c7f29cf0e84ea Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 10:38:36 -0500 Subject: [PATCH 01/72] feat: add CuSessionFinalized IPC message and QA metadata to CuSessionCreate (#6907) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../__snapshots__/ipc-snapshot.test.ts.snap | 10 ++++++ assistant/src/__tests__/ipc-snapshot.test.ts | 7 ++++ assistant/src/daemon/handlers/computer-use.ts | 20 +++++++++++ .../src/daemon/ipc-contract-inventory.json | 2 ++ assistant/src/daemon/ipc-contract.ts | 25 ++++++++++++++ .../IPC/Generated/IPCContractGenerated.swift | 34 ++++++++++++++----- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap index f858561b64f..2ff5da1bd86 100644 --- a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +++ b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap @@ -2499,3 +2499,13 @@ exports[`IPC message snapshots ServerMessage types tool_names_list_response seri "type": "tool_names_list_response", } `; + +exports[`IPC message snapshots ClientMessage types cu_session_finalized serializes to expected JSON 1`] = ` +{ + "sessionId": "cu-sess-001", + "status": "completed", + "stepCount": 5, + "summary": "Task completed successfully", + "type": "cu_session_finalized", +} +`; diff --git a/assistant/src/__tests__/ipc-snapshot.test.ts b/assistant/src/__tests__/ipc-snapshot.test.ts index 9cd92ca86a4..a0e59b7067c 100644 --- a/assistant/src/__tests__/ipc-snapshot.test.ts +++ b/assistant/src/__tests__/ipc-snapshot.test.ts @@ -104,6 +104,13 @@ const clientMessages: Record = { type: 'cu_session_abort', sessionId: 'cu-sess-001', }, + cu_session_finalized: { + type: 'cu_session_finalized', + sessionId: 'cu-sess-001', + status: 'completed', + summary: 'Task completed successfully', + stepCount: 5, + }, cu_observation: { type: 'cu_observation', sessionId: 'cu-sess-001', diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 0567047da6e..c8da4d2fd58 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -7,6 +7,7 @@ import { readBlob, deleteBlob, validateBlobKindEncoding } from '../ipc-blob-stor import type { CuSessionCreate, CuSessionAbort, + CuSessionFinalized, CuObservation, ServerMessage, } from '../ipc-protocol.js'; @@ -180,8 +181,27 @@ export async function handleCuObservation( }); } +export function handleCuSessionFinalized( + msg: CuSessionFinalized, + _socket: net.Socket, + _ctx: HandlerContext, +): void { + log.info( + { + sessionId: msg.sessionId, + status: msg.status, + stepCount: msg.stepCount, + hasRecording: !!msg.recording, + recordingSizeBytes: msg.recording?.sizeBytes, + recordingDurationMs: msg.recording?.durationMs, + }, + 'CU session finalized by client', + ); +} + export const computerUseHandlers = defineHandlers({ cu_session_create: handleCuSessionCreate, cu_session_abort: handleCuSessionAbort, + cu_session_finalized: handleCuSessionFinalized, cu_observation: handleCuObservation, }); diff --git a/assistant/src/daemon/ipc-contract-inventory.json b/assistant/src/daemon/ipc-contract-inventory.json index 525ea1a29a0..4d113dd3501 100644 --- a/assistant/src/daemon/ipc-contract-inventory.json +++ b/assistant/src/daemon/ipc-contract-inventory.json @@ -23,6 +23,7 @@ "CuObservation", "CuSessionAbort", "CuSessionCreate", + "CuSessionFinalized", "DeleteQueuedMessage", "DiagnosticsExportRequest", "DictationRequest", @@ -275,6 +276,7 @@ "cu_observation", "cu_session_abort", "cu_session_create", + "cu_session_finalized", "delete_queued_message", "diagnostics_export_request", "dictation_request", diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 82eaef83487..2aafcf5bcdc 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -177,6 +177,10 @@ export interface CuSessionCreate { screenHeight: number; attachments?: UserMessageAttachment[]; interactionType?: 'computer_use' | 'text_qa'; + /** Origin chat session for result injection (QA workflow). */ + reportToSessionId?: string; + /** Marks this CU run as a QA/test workflow. */ + qaMode?: boolean; } export interface CuSessionAbort { @@ -184,6 +188,26 @@ export interface CuSessionAbort { sessionId: string; } +export interface CuSessionFinalized { + type: 'cu_session_finalized'; + sessionId: string; + status: 'completed' | 'responded' | 'failed' | 'cancelled'; + summary: string; + stepCount: number; + recording?: { + localPath: string; + mimeType: 'video/mp4'; + sizeBytes: number; + durationMs: number; + width: number; + height: number; + captureScope: 'window' | 'display'; + includeAudio: boolean; + targetBundleId?: string; + expiresAt?: number; + }; +} + export interface CuObservation { type: 'cu_observation'; sessionId: string; @@ -1048,6 +1072,7 @@ export type ClientMessage = | SandboxSetRequest | CuSessionCreate | CuSessionAbort + | CuSessionFinalized | CuObservation | RideShotgunStart | RideShotgunStop diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index f0323a2a90d..390e9b87df7 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -921,16 +921,32 @@ public struct IPCCuSessionCreate: Codable, Sendable { public let screenHeight: Int public let attachments: [IPCUserMessageAttachment]? public let interactionType: String? + /// Origin chat session for result injection (QA workflow). + public let reportToSessionId: String? + /// Marks this CU run as a QA/test workflow. + public let qaMode: Bool? +} - public init(type: String, sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]?, interactionType: String?) { - self.type = type - self.sessionId = sessionId - self.task = task - self.screenWidth = screenWidth - self.screenHeight = screenHeight - self.attachments = attachments - self.interactionType = interactionType - } +public struct IPCCuSessionFinalized: Codable, Sendable { + public let type: String + public let sessionId: String + public let status: String + public let summary: String + public let stepCount: Int + public let recording: IPCCuSessionFinalizedRecording? +} + +public struct IPCCuSessionFinalizedRecording: Codable, Sendable { + public let localPath: String + public let mimeType: String + public let sizeBytes: Int + public let durationMs: Double + public let width: Int + public let height: Int + public let captureScope: String + public let includeAudio: Bool + public let targetBundleId: String? + public let expiresAt: Int? } public struct IPCDaemonStatusMessage: Codable, Sendable { From f04fe3d9e4ceb88ed6c87c80d8fe69720c2119e9 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 10:43:45 -0500 Subject: [PATCH 02/72] feat: implement CU session finalization handler with QA metadata plumbing (#6909) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../__tests__/cu-session-finalized.test.ts | 245 ++++++++++++++++++ .../handlers-add-trust-rule-metadata.test.ts | 1 + .../handlers-cu-observation-blob.test.ts | 1 + .../__tests__/handlers-ipc-blob-probe.test.ts | 1 + .../__tests__/handlers-slack-config.test.ts | 1 + .../handlers-telegram-config.test.ts | 1 + .../__tests__/handlers-twilio-config.test.ts | 1 + .../__tests__/handlers-twitter-config.test.ts | 1 + .../src/__tests__/ingress-reconcile.test.ts | 1 + .../tool-permission-simulate-handler.test.ts | 1 + .../__tests__/twitter-auth-handler.test.ts | 1 + assistant/src/daemon/handlers/computer-use.ts | 84 +++++- assistant/src/daemon/handlers/index.ts | 1 + assistant/src/daemon/handlers/shared.ts | 9 + assistant/src/daemon/server.ts | 3 + 15 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 assistant/src/__tests__/cu-session-finalized.test.ts diff --git a/assistant/src/__tests__/cu-session-finalized.test.ts b/assistant/src/__tests__/cu-session-finalized.test.ts new file mode 100644 index 00000000000..7aa95e140fc --- /dev/null +++ b/assistant/src/__tests__/cu-session-finalized.test.ts @@ -0,0 +1,245 @@ +import { describe, test, expect, mock } from 'bun:test'; +import * as net from 'node:net'; + +// Mock the conversation store before importing the handler. +const mockConversations = new Map(); +const mockAddedMessages: Array<{ + conversationId: string; + role: string; + content: string; + metadata?: Record; +}> = []; + +mock.module('../memory/conversation-store.js', () => ({ + getConversation: (id: string) => mockConversations.get(id) ?? null, + addMessage: ( + conversationId: string, + role: string, + content: string, + metadata?: Record, + ) => { + const msg = { conversationId, role, content, metadata }; + mockAddedMessages.push(msg); + return { id: 'mock-msg-id', conversationId, role, content, createdAt: Date.now() }; + }, +})); + +// Mock the config loader (required by shared.ts transitively). +mock.module('../config/loader.js', () => ({ + getConfig: () => ({ + provider: 'mock-provider', + permissions: { mode: 'legacy' }, + apiKeys: {}, + sandbox: { enabled: false }, + timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 }, + skills: { load: { extraDirs: [] } }, + secretDetection: { enabled: false }, + memory: {}, + rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 }, + }), + invalidateConfigCache: () => {}, +})); + +import type { CuSessionFinalized, ServerMessage } from '../daemon/ipc-contract.js'; +import type { HandlerContext, CuSessionMetadata } from '../daemon/handlers/shared.js'; +import { handleCuSessionFinalized } from '../daemon/handlers/computer-use.js'; +import type { ComputerUseSession } from '../daemon/computer-use-session.js'; + +/** Create a minimal HandlerContext for testing. */ +function makeCtx(overrides?: Partial): HandlerContext { + return { + sessions: new Map(), + socketToSession: new Map(), + cuSessions: new Map(), + socketToCuSession: new Map(), + cuSessionMetadata: new Map(), + cuObservationParseSequence: new Map(), + socketSandboxOverride: new Map(), + sharedRequestTimestamps: [], + debounceTimers: new Map() as unknown as HandlerContext['debounceTimers'], + suppressConfigReload: false, + setSuppressConfigReload: () => {}, + updateConfigFingerprint: () => {}, + send: () => {}, + broadcast: () => {}, + clearAllSessions: () => 0, + getOrCreateSession: async () => { throw new Error('not implemented in test'); }, + touchSession: () => {}, + ...overrides, + }; +} + +describe('handleCuSessionFinalized', () => { + test('injects summary into reporting session when reportToSessionId is set', () => { + // Set up a reporting session conversation. + const reportSessionId = 'report-session-1'; + mockConversations.set(reportSessionId, { id: reportSessionId }); + mockAddedMessages.length = 0; + + // Create a mock socket for the reporting session. + const reportSocket = new net.Socket(); + const sentMessages: Array<{ socket: net.Socket; msg: ServerMessage }> = []; + + const ctx = makeCtx({ + send: (socket: net.Socket, msg: ServerMessage) => { sentMessages.push({ socket, msg }); }, + socketToSession: new Map([[reportSocket, reportSessionId]]), + }); + + // Simulate a CU session with metadata. + const cuSessionId = 'cu-session-1'; + ctx.cuSessions.set(cuSessionId, {} as ComputerUseSession); + ctx.cuSessionMetadata.set(cuSessionId, { + reportToSessionId: reportSessionId, + qaMode: true, + }); + + const msg: CuSessionFinalized = { + type: 'cu_session_finalized', + sessionId: cuSessionId, + status: 'completed', + summary: 'QA test passed: login flow works correctly.', + stepCount: 5, + }; + + handleCuSessionFinalized(msg, new net.Socket(), ctx); + + // Verify the assistant message was persisted. + expect(mockAddedMessages.length).toBe(1); + expect(mockAddedMessages[0].conversationId).toBe(reportSessionId); + expect(mockAddedMessages[0].role).toBe('assistant'); + const parsedContent = JSON.parse(mockAddedMessages[0].content); + expect(parsedContent).toEqual([{ type: 'text', text: msg.summary }]); + expect(mockAddedMessages[0].metadata).toMatchObject({ + source: 'cu_session_finalized', + cuSessionId, + cuStatus: 'completed', + cuStepCount: 5, + qaMode: true, + }); + + // Verify IPC messages were sent to the reporting socket. + expect(sentMessages.length).toBe(2); + expect(sentMessages[0].msg).toMatchObject({ + type: 'assistant_text_delta', + text: msg.summary, + sessionId: reportSessionId, + }); + expect(sentMessages[1].msg).toMatchObject({ + type: 'message_complete', + sessionId: reportSessionId, + }); + expect(sentMessages[0].socket).toBe(reportSocket); + + // Verify CU session state was cleaned up. + expect(ctx.cuSessions.has(cuSessionId)).toBe(false); + expect(ctx.cuSessionMetadata.has(cuSessionId)).toBe(false); + }); + + test('cleans up CU session state even without reportToSessionId', () => { + const ctx = makeCtx(); + const cuSessionId = 'cu-session-2'; + ctx.cuSessions.set(cuSessionId, {} as ComputerUseSession); + // No metadata set — this is a non-QA CU session. + + const msg: CuSessionFinalized = { + type: 'cu_session_finalized', + sessionId: cuSessionId, + status: 'completed', + summary: 'Task done.', + stepCount: 3, + }; + + mockAddedMessages.length = 0; + handleCuSessionFinalized(msg, new net.Socket(), ctx); + + // No message should be persisted (no reportToSessionId). + expect(mockAddedMessages.length).toBe(0); + + // CU session state should still be cleaned up. + expect(ctx.cuSessions.has(cuSessionId)).toBe(false); + }); + + test('handles missing reporting conversation gracefully', () => { + const ctx = makeCtx(); + const cuSessionId = 'cu-session-3'; + ctx.cuSessions.set(cuSessionId, {} as ComputerUseSession); + ctx.cuSessionMetadata.set(cuSessionId, { + reportToSessionId: 'nonexistent-session', + qaMode: true, + }); + + // Make sure the conversation does NOT exist in the mock store. + mockConversations.delete('nonexistent-session'); + mockAddedMessages.length = 0; + + const msg: CuSessionFinalized = { + type: 'cu_session_finalized', + sessionId: cuSessionId, + status: 'failed', + summary: 'QA test failed.', + stepCount: 2, + }; + + // Should not throw even if the conversation is missing. + handleCuSessionFinalized(msg, new net.Socket(), ctx); + + expect(mockAddedMessages.length).toBe(0); + expect(ctx.cuSessions.has(cuSessionId)).toBe(false); + expect(ctx.cuSessionMetadata.has(cuSessionId)).toBe(false); + }); + + test('logs recording metadata without crashing', () => { + const reportSessionId = 'report-session-rec'; + mockConversations.set(reportSessionId, { id: reportSessionId }); + mockAddedMessages.length = 0; + + const ctx = makeCtx(); + const cuSessionId = 'cu-session-rec'; + ctx.cuSessions.set(cuSessionId, {} as ComputerUseSession); + ctx.cuSessionMetadata.set(cuSessionId, { + reportToSessionId: reportSessionId, + }); + + const msg: CuSessionFinalized = { + type: 'cu_session_finalized', + sessionId: cuSessionId, + status: 'completed', + summary: 'Done with recording.', + stepCount: 4, + recording: { + localPath: '/tmp/recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 1024000, + durationMs: 30000, + width: 1920, + height: 1080, + captureScope: 'window', + includeAudio: false, + }, + }; + + handleCuSessionFinalized(msg, new net.Socket(), ctx); + + // Message should be persisted with recording path in metadata. + expect(mockAddedMessages.length).toBe(1); + expect(mockAddedMessages[0].metadata).toMatchObject({ + recordingPath: '/tmp/recording.mp4', + }); + }); + + test('stores and retrieves CU session metadata', () => { + const ctx = makeCtx(); + + const cuSessionId = 'cu-meta-test'; + const meta: CuSessionMetadata = { + reportToSessionId: 'parent-123', + qaMode: true, + }; + ctx.cuSessionMetadata.set(cuSessionId, meta); + + const stored = ctx.cuSessionMetadata.get(cuSessionId); + expect(stored).toEqual(meta); + expect(stored?.reportToSessionId).toBe('parent-123'); + expect(stored?.qaMode).toBe(true); + }); +}); diff --git a/assistant/src/__tests__/handlers-add-trust-rule-metadata.test.ts b/assistant/src/__tests__/handlers-add-trust-rule-metadata.test.ts index 88b585fba47..508f1522217 100644 --- a/assistant/src/__tests__/handlers-add-trust-rule-metadata.test.ts +++ b/assistant/src/__tests__/handlers-add-trust-rule-metadata.test.ts @@ -71,6 +71,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/handlers-cu-observation-blob.test.ts b/assistant/src/__tests__/handlers-cu-observation-blob.test.ts index 1f845194baa..aee0d11b651 100644 --- a/assistant/src/__tests__/handlers-cu-observation-blob.test.ts +++ b/assistant/src/__tests__/handlers-cu-observation-blob.test.ts @@ -84,6 +84,7 @@ function createTestContext(sessionId: string): { socketToSession: new Map(), cuSessions, socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/handlers-ipc-blob-probe.test.ts b/assistant/src/__tests__/handlers-ipc-blob-probe.test.ts index 13e44f0f870..006e990358f 100644 --- a/assistant/src/__tests__/handlers-ipc-blob-probe.test.ts +++ b/assistant/src/__tests__/handlers-ipc-blob-probe.test.ts @@ -61,6 +61,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/handlers-slack-config.test.ts b/assistant/src/__tests__/handlers-slack-config.test.ts index 1c597275026..2321808a8ec 100644 --- a/assistant/src/__tests__/handlers-slack-config.test.ts +++ b/assistant/src/__tests__/handlers-slack-config.test.ts @@ -91,6 +91,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/handlers-telegram-config.test.ts b/assistant/src/__tests__/handlers-telegram-config.test.ts index 0d74bae60f5..47c51b93ad8 100644 --- a/assistant/src/__tests__/handlers-telegram-config.test.ts +++ b/assistant/src/__tests__/handlers-telegram-config.test.ts @@ -138,6 +138,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/handlers-twilio-config.test.ts b/assistant/src/__tests__/handlers-twilio-config.test.ts index 14a4b963e97..2bb1f871e2e 100644 --- a/assistant/src/__tests__/handlers-twilio-config.test.ts +++ b/assistant/src/__tests__/handlers-twilio-config.test.ts @@ -152,6 +152,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/handlers-twitter-config.test.ts b/assistant/src/__tests__/handlers-twitter-config.test.ts index 1a58a50a59e..6a1b4e05f05 100644 --- a/assistant/src/__tests__/handlers-twitter-config.test.ts +++ b/assistant/src/__tests__/handlers-twitter-config.test.ts @@ -124,6 +124,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/ingress-reconcile.test.ts b/assistant/src/__tests__/ingress-reconcile.test.ts index 0532b195001..8332db2ff90 100644 --- a/assistant/src/__tests__/ingress-reconcile.test.ts +++ b/assistant/src/__tests__/ingress-reconcile.test.ts @@ -89,6 +89,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/tool-permission-simulate-handler.test.ts b/assistant/src/__tests__/tool-permission-simulate-handler.test.ts index 9083f4dc8fa..f288e3f3d3c 100644 --- a/assistant/src/__tests__/tool-permission-simulate-handler.test.ts +++ b/assistant/src/__tests__/tool-permission-simulate-handler.test.ts @@ -69,6 +69,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/__tests__/twitter-auth-handler.test.ts b/assistant/src/__tests__/twitter-auth-handler.test.ts index 21da7347a35..006e65e843c 100644 --- a/assistant/src/__tests__/twitter-auth-handler.test.ts +++ b/assistant/src/__tests__/twitter-auth-handler.test.ts @@ -160,6 +160,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } { socketToSession: new Map(), cuSessions: new Map(), socketToCuSession: new Map(), + cuSessionMetadata: new Map(), cuObservationParseSequence: new Map(), socketSandboxOverride: new Map(), sharedRequestTimestamps: [], diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index c8da4d2fd58..be48ce9d72f 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -11,7 +11,8 @@ import type { CuObservation, ServerMessage, } from '../ipc-protocol.js'; -import { log, defineHandlers, type HandlerContext } from './shared.js'; +import * as conversationStore from '../../memory/conversation-store.js'; +import { log, defineHandlers, findSocketForSession, type HandlerContext, type CuSessionMetadata } from './shared.js'; const cuObservationSequenceBySession = new Map(); @@ -25,6 +26,7 @@ function removeCuSessionReferences( return; } ctx.cuSessions.delete(sessionId); + ctx.cuSessionMetadata.delete(sessionId); cuObservationSequenceBySession.delete(sessionId); ctx.cuObservationParseSequence.delete(sessionId); for (const [sock, ids] of ctx.socketToCuSession) { @@ -79,6 +81,15 @@ export function handleCuSessionCreate( ctx.cuSessions.set(msg.sessionId, session); + // Store QA metadata so handleCuSessionFinalized can inject results + // into the originating chat session. + if (msg.reportToSessionId || msg.qaMode) { + const meta: CuSessionMetadata = {}; + if (msg.reportToSessionId) meta.reportToSessionId = msg.reportToSessionId; + if (msg.qaMode) meta.qaMode = msg.qaMode; + ctx.cuSessionMetadata.set(msg.sessionId, meta); + } + // Track all CU sessions per socket so disconnect cleans up all of them let sessionIds = ctx.socketToCuSession.get(socket); if (!sessionIds) { @@ -184,8 +195,10 @@ export async function handleCuObservation( export function handleCuSessionFinalized( msg: CuSessionFinalized, _socket: net.Socket, - _ctx: HandlerContext, + ctx: HandlerContext, ): void { + const meta = ctx.cuSessionMetadata.get(msg.sessionId); + log.info( { sessionId: msg.sessionId, @@ -194,9 +207,76 @@ export function handleCuSessionFinalized( hasRecording: !!msg.recording, recordingSizeBytes: msg.recording?.sizeBytes, recordingDurationMs: msg.recording?.durationMs, + reportToSessionId: meta?.reportToSessionId, + qaMode: meta?.qaMode, }, 'CU session finalized by client', ); + + // If recording metadata is present, log it for future M3 file-backed attachment support. + if (msg.recording) { + log.info( + { + sessionId: msg.sessionId, + localPath: msg.recording.localPath, + mimeType: msg.recording.mimeType, + sizeBytes: msg.recording.sizeBytes, + durationMs: msg.recording.durationMs, + width: msg.recording.width, + height: msg.recording.height, + captureScope: msg.recording.captureScope, + }, + 'CU session recording metadata (stored for M3)', + ); + } + + // Inject a summary message into the originating chat session if configured. + if (meta?.reportToSessionId && msg.summary) { + const reportSessionId = meta.reportToSessionId; + const reportSocket = findSocketForSession(reportSessionId, ctx); + + // Persist the assistant message in the conversation store so it appears + // in history even if the client is not currently connected. + const conversation = conversationStore.getConversation(reportSessionId); + if (conversation) { + const assistantContent = JSON.stringify([{ type: 'text', text: msg.summary }]); + conversationStore.addMessage(reportSessionId, 'assistant', assistantContent, { + source: 'cu_session_finalized', + cuSessionId: msg.sessionId, + cuStatus: msg.status, + cuStepCount: msg.stepCount, + qaMode: meta.qaMode ?? false, + ...(msg.recording ? { recordingPath: msg.recording.localPath } : {}), + }); + + // If the reporting session has a connected client, stream the summary + // so it appears in real time. + if (reportSocket) { + ctx.send(reportSocket, { + type: 'assistant_text_delta', + text: msg.summary, + sessionId: reportSessionId, + }); + ctx.send(reportSocket, { + type: 'message_complete', + sessionId: reportSessionId, + }); + } + + log.info( + { cuSessionId: msg.sessionId, reportToSessionId: reportSessionId }, + 'Injected CU finalization summary into reporting session', + ); + } else { + log.warn( + { cuSessionId: msg.sessionId, reportToSessionId: reportSessionId }, + 'Reporting session conversation not found; summary not persisted', + ); + } + } + + // Clean up all CU session state. + removeCuSessionReferences(ctx, msg.sessionId); } export const computerUseHandlers = defineHandlers({ diff --git a/assistant/src/daemon/handlers/index.ts b/assistant/src/daemon/handlers/index.ts index 87004c3a374..e004f5b3a6c 100644 --- a/assistant/src/daemon/handlers/index.ts +++ b/assistant/src/daemon/handlers/index.ts @@ -27,6 +27,7 @@ import { dictationHandlers } from './dictation.js'; // Re-export types and utilities for backwards compatibility export type { HandlerContext, + CuSessionMetadata, SessionCreateOptions, HistoryToolCall, HistorySurface, diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index 167ad1ea060..ef084d730c9 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -104,6 +104,14 @@ export interface SessionCreateOptions { strictPrivateSideEffects?: boolean; } +/** Metadata stored alongside a CU session for QA workflow plumbing. */ +export interface CuSessionMetadata { + /** Origin chat session to inject results into on finalization. */ + reportToSessionId?: string; + /** Whether this CU run is a QA/test workflow. */ + qaMode?: boolean; +} + /** * Shared context that handlers need from the DaemonServer. * Keeps handlers decoupled from the server class itself. @@ -113,6 +121,7 @@ export interface HandlerContext { socketToSession: Map; cuSessions: Map; socketToCuSession: Map>; + cuSessionMetadata: Map; cuObservationParseSequence: Map; socketSandboxOverride: Map; sharedRequestTimestamps: number[]; diff --git a/assistant/src/daemon/server.ts b/assistant/src/daemon/server.ts index eb1e6a04011..5cdc4cb687c 100644 --- a/assistant/src/daemon/server.ts +++ b/assistant/src/daemon/server.ts @@ -60,6 +60,7 @@ export class DaemonServer { private socketToSession = new Map(); private cuSessions = new Map(); private socketToCuSession = new Map>(); + private cuSessionMetadata = new Map(); private connectedSockets = new Set(); private socketSandboxOverride = new Map(); private cuObservationParseSequence = new Map(); @@ -298,6 +299,7 @@ export class DaemonServer { } this.cuSessions.clear(); this.socketToCuSession.clear(); + this.cuSessionMetadata.clear(); for (const socket of this.connectedSockets) { socket.destroy(); @@ -612,6 +614,7 @@ export class DaemonServer { socketToSession: this.socketToSession, cuSessions: this.cuSessions, socketToCuSession: this.socketToCuSession, + cuSessionMetadata: this.cuSessionMetadata, cuObservationParseSequence: this.cuObservationParseSequence, socketSandboxOverride: this.socketSandboxOverride, sharedRequestTimestamps: this.sharedRequestTimestamps, From 99d922d6552adcb25898fb856d0d08f33d309608 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 10:45:23 -0500 Subject: [PATCH 03/72] fix: update IPCCuSessionCreate init and add Ms suffix to INT_PATTERNS (#6910) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/scripts/ipc/generate-swift.ts | 1 + clients/shared/IPC/Generated/IPCContractGenerated.swift | 6 +++--- clients/shared/IPC/IPCMessages.swift | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/assistant/scripts/ipc/generate-swift.ts b/assistant/scripts/ipc/generate-swift.ts index 63fdfc6cddd..6e2e3e9248b 100644 --- a/assistant/scripts/ipc/generate-swift.ts +++ b/assistant/scripts/ipc/generate-swift.ts @@ -180,6 +180,7 @@ const INT_PATTERNS = [ /[Ii]ndex$/, /[Ee]xpected$/, /[Uu]ndos$/, + /Ms$/, ]; function shouldBeInt(propName: string): boolean { diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 390e9b87df7..ff679d86690 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -940,7 +940,7 @@ public struct IPCCuSessionFinalizedRecording: Codable, Sendable { public let localPath: String public let mimeType: String public let sizeBytes: Int - public let durationMs: Double + public let durationMs: Int public let width: Int public let height: Int public let captureScope: String @@ -2012,7 +2012,7 @@ public struct IPCMemoryRecalled: Codable, Sendable { public let selectedCount: Int public let rerankApplied: Bool public let injectedTokens: Int - public let latencyMs: Double + public let latencyMs: Int public let topCandidates: [IPCMemoryRecalledCandidateDebug] public init(type: String, provider: String, model: String, lexicalHits: Double, semanticHits: Double, recencyHits: Double, entityHits: Double, relationSeedEntityCount: Int?, relationTraversedEdgeCount: Int?, relationNeighborEntityCount: Int?, relationExpandedItemCount: Int?, earlyTerminated: Bool?, mergedCount: Int, selectedCount: Int, rerankApplied: Bool, injectedTokens: Int, latencyMs: Double, topCandidates: [IPCMemoryRecalledCandidateDebug]) { @@ -2066,7 +2066,7 @@ public struct IPCMemoryStatus: Codable, Sendable { public let model: String? public let conflictsPending: Double public let conflictsResolved: Double - public let oldestPendingConflictAgeMs: Double? + public let oldestPendingConflictAgeMs: Int? public let cleanupResolvedJobsPending: Double public let cleanupSupersededJobsPending: Double public let cleanupResolvedJobsCompleted24h: Double diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index dd28b8adde6..ae8dcd9e188 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -148,8 +148,8 @@ extension IPCUserMessageAttachment { public typealias CuSessionCreateMessage = IPCCuSessionCreate extension IPCCuSessionCreate { - public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?) { - self.init(type: "cu_session_create", sessionId: sessionId, task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, interactionType: interactionType) + public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?, reportToSessionId: String? = nil, qaMode: Bool? = nil) { + self.init(type: "cu_session_create", sessionId: sessionId, task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, interactionType: interactionType, reportToSessionId: reportToSessionId, qaMode: qaMode) } } From 7a02a9c01142b79f9fecd8e56be183e280683fab Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 10:50:03 -0500 Subject: [PATCH 04/72] feat: add file-backed attachment storage and content streaming endpoint (#6911) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../attachment-content-route.test.ts | 210 ++++++++++++ .../__tests__/file-backed-attachments.test.ts | 319 ++++++++++++++++++ .../fixtures/media-reuse-fixtures.ts | 12 + assistant/src/memory/attachments-store.ts | 114 ++++++- assistant/src/memory/db-init.ts | 4 + assistant/src/memory/schema.ts | 4 + assistant/src/runtime/http-server.ts | 7 + .../src/runtime/routes/attachment-routes.ts | 99 ++++++ assistant/src/tools/assets/search.ts | 23 +- 9 files changed, 787 insertions(+), 5 deletions(-) create mode 100644 assistant/src/__tests__/attachment-content-route.test.ts create mode 100644 assistant/src/__tests__/file-backed-attachments.test.ts diff --git a/assistant/src/__tests__/attachment-content-route.test.ts b/assistant/src/__tests__/attachment-content-route.test.ts new file mode 100644 index 00000000000..57168a19116 --- /dev/null +++ b/assistant/src/__tests__/attachment-content-route.test.ts @@ -0,0 +1,210 @@ +import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const testDir = mkdtempSync(join(tmpdir(), 'attach-content-test-')); + +mock.module('../util/platform.js', () => ({ + getDataDir: () => testDir, + isMacOS: () => process.platform === 'darwin', + isLinux: () => process.platform === 'linux', + isWindows: () => process.platform === 'win32', + getSocketPath: () => join(testDir, 'test.sock'), + getPidPath: () => join(testDir, 'test.pid'), + getDbPath: () => join(testDir, 'test.db'), + getLogPath: () => join(testDir, 'test.log'), + ensureDataDir: () => {}, + getRootDir: () => testDir, +})); + +mock.module('../util/logger.js', () => ({ + getLogger: () => new Proxy({} as Record, { + get: () => () => {}, + }), +})); + +mock.module('../config/loader.js', () => ({ + getConfig: () => ({ + model: 'test', + provider: 'test', + apiKeys: {}, + memory: { enabled: false }, + rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 }, + }), +})); + +import { initializeDb, getDb, resetDb } from '../memory/db.js'; +import { + uploadAttachment, + createFileBackedAttachment, +} from '../memory/attachments-store.js'; +import { handleGetAttachmentContent } from '../runtime/routes/attachment-routes.js'; + +initializeDb(); + +afterAll(() => { + resetDb(); + try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ } +}); + +function resetTables() { + const db = getDb(); + db.run('DELETE FROM message_attachments'); + db.run('DELETE FROM attachments'); +} + +// --------------------------------------------------------------------------- +// handleGetAttachmentContent — full file content +// --------------------------------------------------------------------------- + +describe('handleGetAttachmentContent — file-backed', () => { + beforeEach(resetTables); + + test('returns full file content for non-range request', async () => { + const filePath = join(testDir, 'test-video.mp4'); + const content = Buffer.from('fake video content for testing'); + writeFileSync(filePath, content); + + const attachment = createFileBackedAttachment({ + filename: 'test-video.mp4', + mimeType: 'video/mp4', + sizeBytes: content.length, + filePath, + }); + + const req = new Request('http://localhost/v1/attachments/' + attachment.id + '/content'); + const res = handleGetAttachmentContent(attachment.id, req); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toBe('video/mp4'); + expect(res.headers.get('Content-Length')).toBe(String(content.length)); + expect(res.headers.get('Accept-Ranges')).toBe('bytes'); + expect(res.headers.get('Content-Disposition')).toBe('inline'); + + const body = await res.arrayBuffer(); + expect(Buffer.from(body).toString()).toBe(content.toString()); + }); + + test('returns partial content for range request', async () => { + const filePath = join(testDir, 'test-range.mp4'); + const content = Buffer.from('0123456789abcdef'); + writeFileSync(filePath, content); + + const attachment = createFileBackedAttachment({ + filename: 'test-range.mp4', + mimeType: 'video/mp4', + sizeBytes: content.length, + filePath, + }); + + const req = new Request('http://localhost/v1/attachments/' + attachment.id + '/content', { + headers: { Range: 'bytes=4-9' }, + }); + const res = handleGetAttachmentContent(attachment.id, req); + + expect(res.status).toBe(206); + expect(res.headers.get('Content-Range')).toBe(`bytes 4-9/${content.length}`); + expect(res.headers.get('Content-Length')).toBe('6'); + + const body = await res.arrayBuffer(); + expect(Buffer.from(body).toString()).toBe('456789'); + }); + + test('returns range with open-ended range header', async () => { + const filePath = join(testDir, 'test-open-range.mp4'); + const content = Buffer.from('abcdefghij'); + writeFileSync(filePath, content); + + const attachment = createFileBackedAttachment({ + filename: 'test-open-range.mp4', + mimeType: 'video/mp4', + sizeBytes: content.length, + filePath, + }); + + const req = new Request('http://localhost/v1/attachments/' + attachment.id + '/content', { + headers: { Range: 'bytes=5-' }, + }); + const res = handleGetAttachmentContent(attachment.id, req); + + expect(res.status).toBe(206); + expect(res.headers.get('Content-Range')).toBe(`bytes 5-9/${content.length}`); + expect(res.headers.get('Content-Length')).toBe('5'); + + const body = await res.arrayBuffer(); + expect(Buffer.from(body).toString()).toBe('fghij'); + }); + + test('returns 416 for unsatisfiable range', () => { + const filePath = join(testDir, 'test-416.mp4'); + const content = Buffer.from('short'); + writeFileSync(filePath, content); + + const attachment = createFileBackedAttachment({ + filename: 'test-416.mp4', + mimeType: 'video/mp4', + sizeBytes: content.length, + filePath, + }); + + const req = new Request('http://localhost/v1/attachments/' + attachment.id + '/content', { + headers: { Range: 'bytes=100-200' }, + }); + const res = handleGetAttachmentContent(attachment.id, req); + + expect(res.status).toBe(416); + }); + + test('returns 404 when file is missing from disk', () => { + const attachment = createFileBackedAttachment({ + filename: 'missing.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/nonexistent/path/missing.mp4', + }); + + const req = new Request('http://localhost/v1/attachments/' + attachment.id + '/content'); + const res = handleGetAttachmentContent(attachment.id, req); + + expect(res.status).toBe(404); + }); +}); + +// --------------------------------------------------------------------------- +// handleGetAttachmentContent — 404 for non-existent +// --------------------------------------------------------------------------- + +describe('handleGetAttachmentContent — 404', () => { + beforeEach(resetTables); + + test('returns 404 for non-existent attachment', () => { + const req = new Request('http://localhost/v1/attachments/nonexistent/content'); + const res = handleGetAttachmentContent('nonexistent', req); + expect(res.status).toBe(404); + }); +}); + +// --------------------------------------------------------------------------- +// handleGetAttachmentContent — inline_base64 fallback +// --------------------------------------------------------------------------- + +describe('handleGetAttachmentContent — inline_base64 fallback', () => { + beforeEach(resetTables); + + test('decodes and returns inline base64 content', async () => { + const originalText = 'hello world'; + const base64 = Buffer.from(originalText).toString('base64'); + const stored = uploadAttachment('hello.txt', 'text/plain', base64); + + const req = new Request('http://localhost/v1/attachments/' + stored.id + '/content'); + const res = handleGetAttachmentContent(stored.id, req); + + expect(res.status).toBe(200); + expect(res.headers.get('Content-Type')).toBe('text/plain'); + expect(res.headers.get('Accept-Ranges')).toBe('bytes'); + + const body = await res.text(); + expect(body).toBe(originalText); + }); +}); diff --git a/assistant/src/__tests__/file-backed-attachments.test.ts b/assistant/src/__tests__/file-backed-attachments.test.ts new file mode 100644 index 00000000000..06ed23015de --- /dev/null +++ b/assistant/src/__tests__/file-backed-attachments.test.ts @@ -0,0 +1,319 @@ +import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const testDir = mkdtempSync(join(tmpdir(), 'file-backed-attach-test-')); + +mock.module('../util/platform.js', () => ({ + getDataDir: () => testDir, + isMacOS: () => process.platform === 'darwin', + isLinux: () => process.platform === 'linux', + isWindows: () => process.platform === 'win32', + getSocketPath: () => join(testDir, 'test.sock'), + getPidPath: () => join(testDir, 'test.pid'), + getDbPath: () => join(testDir, 'test.db'), + getLogPath: () => join(testDir, 'test.log'), + ensureDataDir: () => {}, + getRootDir: () => testDir, +})); + +mock.module('../util/logger.js', () => ({ + getLogger: () => new Proxy({} as Record, { + get: () => () => {}, + }), +})); + +mock.module('../config/loader.js', () => ({ + getConfig: () => ({ + model: 'test', + provider: 'test', + apiKeys: {}, + memory: { enabled: false }, + rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 }, + }), +})); + +import { initializeDb, getDb, resetDb } from '../memory/db.js'; +import { + uploadAttachment, + getAttachmentById, + getAttachmentsByIds, + createFileBackedAttachment, + getExpiredFileAttachments, + deleteFileBackedAttachment, +} from '../memory/attachments-store.js'; + +initializeDb(); + +afterAll(() => { + resetDb(); + try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ } +}); + +function resetTables() { + const db = getDb(); + db.run('DELETE FROM message_attachments'); + db.run('DELETE FROM attachments'); +} + +// --------------------------------------------------------------------------- +// createFileBackedAttachment +// --------------------------------------------------------------------------- + +describe('createFileBackedAttachment', () => { + beforeEach(resetTables); + + test('creates correct DB record with file metadata', () => { + const result = createFileBackedAttachment({ + filename: 'recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 50_000_000, + filePath: '/data/recordings/recording.mp4', + sha256: 'abc123def456', + expiresAt: Date.now() + 86400000, + }); + + expect(result.id).toBeDefined(); + expect(result.originalFilename).toBe('recording.mp4'); + expect(result.mimeType).toBe('video/mp4'); + expect(result.sizeBytes).toBe(50_000_000); + expect(result.kind).toBe('video'); + expect(result.storageKind).toBe('file'); + expect(result.filePath).toBe('/data/recordings/recording.mp4'); + expect(result.sha256).toBe('abc123def456'); + expect(result.expiresAt).toBeGreaterThan(0); + expect(result.createdAt).toBeGreaterThan(0); + }); + + test('handles optional fields gracefully', () => { + const result = createFileBackedAttachment({ + filename: 'screenshot.png', + mimeType: 'image/png', + sizeBytes: 1024, + filePath: '/data/screenshots/shot.png', + }); + + expect(result.sha256).toBeNull(); + expect(result.expiresAt).toBeNull(); + expect(result.thumbnailBase64).toBeNull(); + }); + + test('stores thumbnail when provided', () => { + const result = createFileBackedAttachment({ + filename: 'video.mp4', + mimeType: 'video/mp4', + sizeBytes: 10_000, + filePath: '/data/video.mp4', + thumbnailBase64: 'iVBORw0KGgoAAAANSUh', + }); + + expect(result.thumbnailBase64).toBe('iVBORw0KGgoAAAANSUh'); + }); + + test('classifies kind from mime type', () => { + const image = createFileBackedAttachment({ + filename: 'pic.png', + mimeType: 'image/png', + sizeBytes: 100, + filePath: '/data/pic.png', + }); + expect(image.kind).toBe('image'); + + const video = createFileBackedAttachment({ + filename: 'clip.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/data/clip.mp4', + }); + expect(video.kind).toBe('video'); + + const doc = createFileBackedAttachment({ + filename: 'doc.pdf', + mimeType: 'application/pdf', + sizeBytes: 100, + filePath: '/data/doc.pdf', + }); + expect(doc.kind).toBe('document'); + }); +}); + +// --------------------------------------------------------------------------- +// getAttachmentById returns file metadata for file-backed attachments +// --------------------------------------------------------------------------- + +describe('getAttachmentById with file-backed attachments', () => { + beforeEach(resetTables); + + test('returns file metadata for file-backed attachments', () => { + const created = createFileBackedAttachment({ + filename: 'recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 50_000_000, + filePath: '/data/recording.mp4', + sha256: 'abc123', + expiresAt: 1234567890, + }); + + const fetched = getAttachmentById(created.id); + expect(fetched).not.toBeNull(); + expect(fetched!.storageKind).toBe('file'); + expect(fetched!.filePath).toBe('/data/recording.mp4'); + expect(fetched!.sha256).toBe('abc123'); + expect(fetched!.expiresAt).toBe(1234567890); + expect(fetched!.dataBase64).toBe(''); + }); + + test('returns inline_base64 for traditional attachments', () => { + const created = uploadAttachment('chart.png', 'image/png', 'iVBORw0K'); + + const fetched = getAttachmentById(created.id); + expect(fetched).not.toBeNull(); + expect(fetched!.storageKind).toBe('inline_base64'); + expect(fetched!.filePath).toBeNull(); + expect(fetched!.sha256).toBeNull(); + expect(fetched!.expiresAt).toBeNull(); + expect(fetched!.dataBase64).toBe('iVBORw0K'); + }); +}); + +// --------------------------------------------------------------------------- +// getAttachmentsByIds with mixed types +// --------------------------------------------------------------------------- + +describe('getAttachmentsByIds with mixed types', () => { + beforeEach(resetTables); + + test('returns both inline and file-backed attachments', () => { + const inline = uploadAttachment('doc.pdf', 'application/pdf', 'JVBER'); + const fileBacked = createFileBackedAttachment({ + filename: 'video.mp4', + mimeType: 'video/mp4', + sizeBytes: 5000, + filePath: '/data/video.mp4', + }); + + const results = getAttachmentsByIds([inline.id, fileBacked.id]); + expect(results).toHaveLength(2); + + const inlineResult = results.find((r) => r.id === inline.id); + expect(inlineResult!.storageKind).toBe('inline_base64'); + expect(inlineResult!.dataBase64).toBe('JVBER'); + + const fileResult = results.find((r) => r.id === fileBacked.id); + expect(fileResult!.storageKind).toBe('file'); + expect(fileResult!.filePath).toBe('/data/video.mp4'); + }); +}); + +// --------------------------------------------------------------------------- +// getExpiredFileAttachments +// --------------------------------------------------------------------------- + +describe('getExpiredFileAttachments', () => { + beforeEach(resetTables); + + test('returns only expired file attachments', () => { + const now = Date.now(); + + // Expired + createFileBackedAttachment({ + filename: 'old.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/data/old.mp4', + expiresAt: now - 10000, + }); + + // Not expired + createFileBackedAttachment({ + filename: 'new.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/data/new.mp4', + expiresAt: now + 86400000, + }); + + // No expiry + createFileBackedAttachment({ + filename: 'permanent.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/data/permanent.mp4', + }); + + // Inline base64 (should never be returned) + uploadAttachment('inline.txt', 'text/plain', 'AAAA'); + + const expired = getExpiredFileAttachments(); + expect(expired).toHaveLength(1); + expect(expired[0].filePath).toBe('/data/old.mp4'); + }); + + test('returns empty when no expired attachments', () => { + createFileBackedAttachment({ + filename: 'future.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/data/future.mp4', + expiresAt: Date.now() + 86400000, + }); + + const expired = getExpiredFileAttachments(); + expect(expired).toHaveLength(0); + }); +}); + +// --------------------------------------------------------------------------- +// deleteFileBackedAttachment +// --------------------------------------------------------------------------- + +describe('deleteFileBackedAttachment', () => { + beforeEach(resetTables); + + test('deletes existing file-backed attachment', () => { + const created = createFileBackedAttachment({ + filename: 'recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 100, + filePath: '/data/recording.mp4', + }); + + const result = deleteFileBackedAttachment(created.id); + expect(result).toBe('deleted'); + + const fetched = getAttachmentById(created.id); + expect(fetched).toBeNull(); + }); + + test('returns not_found for nonexistent attachment', () => { + const result = deleteFileBackedAttachment('nonexistent-id'); + expect(result).toBe('not_found'); + }); +}); + +// --------------------------------------------------------------------------- +// Backward compatibility: existing base64 attachments still work +// --------------------------------------------------------------------------- + +describe('backward compatibility', () => { + beforeEach(resetTables); + + test('existing uploadAttachment still works correctly', () => { + const stored = uploadAttachment('chart.png', 'image/png', 'iVBORw0K'); + expect(stored.id).toBeDefined(); + expect(stored.storageKind).toBe('inline_base64'); + expect(stored.filePath).toBeNull(); + + const fetched = getAttachmentById(stored.id); + expect(fetched).not.toBeNull(); + expect(fetched!.dataBase64).toBe('iVBORw0K'); + expect(fetched!.storageKind).toBe('inline_base64'); + }); + + test('deduplication still works for inline base64', () => { + const first = uploadAttachment('a.png', 'image/png', 'DUPEDATA'); + const second = uploadAttachment('b.png', 'image/png', 'DUPEDATA'); + expect(first.id).toBe(second.id); + }); +}); diff --git a/assistant/src/__tests__/fixtures/media-reuse-fixtures.ts b/assistant/src/__tests__/fixtures/media-reuse-fixtures.ts index fa16c2c9717..44fb0ca0898 100644 --- a/assistant/src/__tests__/fixtures/media-reuse-fixtures.ts +++ b/assistant/src/__tests__/fixtures/media-reuse-fixtures.ts @@ -36,6 +36,10 @@ export const FAKE_SELFIE_ATTACHMENT: StoredAttachment = { sizeBytes: Buffer.from(TINY_PNG_BASE64, 'base64').length, kind: 'image', thumbnailBase64: null, + storageKind: 'inline_base64', + filePath: null, + sha256: null, + expiresAt: null, createdAt: NOW, }; @@ -47,6 +51,10 @@ export const FAKE_DOCUMENT_ATTACHMENT: StoredAttachment = { sizeBytes: 4096, kind: 'document', thumbnailBase64: null, + storageKind: 'inline_base64', + filePath: null, + sha256: null, + expiresAt: null, createdAt: NOW, }; @@ -58,6 +66,10 @@ export const FAKE_PHOTO_ATTACHMENT: StoredAttachment = { sizeBytes: Buffer.from(TINY_JPEG_BASE64, 'base64').length, kind: 'image', thumbnailBase64: null, + storageKind: 'inline_base64', + filePath: null, + sha256: null, + expiresAt: null, createdAt: NOW, }; diff --git a/assistant/src/memory/attachments-store.ts b/assistant/src/memory/attachments-store.ts index 8aa9cd91aad..fc51e39bf82 100644 --- a/assistant/src/memory/attachments-store.ts +++ b/assistant/src/memory/attachments-store.ts @@ -17,6 +17,10 @@ export interface StoredAttachment { sizeBytes: number; kind: string; thumbnailBase64: string | null; + storageKind: 'inline_base64' | 'file'; + filePath: string | null; + sha256: string | null; + expiresAt: number | null; createdAt: number; } @@ -182,6 +186,10 @@ export function uploadAttachment( sizeBytes: attachments.sizeBytes, kind: attachments.kind, thumbnailBase64: attachments.thumbnailBase64, + storageKind: attachments.storageKind, + filePath: attachments.filePath, + sha256: attachments.sha256, + expiresAt: attachments.expiresAt, createdAt: attachments.createdAt, }) .from(attachments) @@ -189,7 +197,7 @@ export function uploadAttachment( .get(); if (existing) { - return existing; + return { ...existing, storageKind: existing.storageKind as 'inline_base64' | 'file' }; } const now = Date.now(); @@ -215,6 +223,10 @@ export function uploadAttachment( sizeBytes, kind, thumbnailBase64: null, + storageKind: 'inline_base64' as const, + filePath: null, + sha256: null, + expiresAt: null, createdAt: now, }; } @@ -281,6 +293,10 @@ export function getAttachmentsByIds( sizeBytes: row.sizeBytes, kind: row.kind, thumbnailBase64: row.thumbnailBase64, + storageKind: row.storageKind as 'inline_base64' | 'file', + filePath: row.filePath, + sha256: row.sha256, + expiresAt: row.expiresAt, dataBase64: row.dataBase64, createdAt: row.createdAt, }); @@ -356,12 +372,16 @@ export function getAttachmentMetadataForMessage( sizeBytes: attachments.sizeBytes, kind: attachments.kind, thumbnailBase64: attachments.thumbnailBase64, + storageKind: attachments.storageKind, + filePath: attachments.filePath, + sha256: attachments.sha256, + expiresAt: attachments.expiresAt, createdAt: attachments.createdAt, }) .from(attachments) .where(eq(attachments.id, link.attachmentId)) .get(); - if (row) results.push(row); + if (row) results.push({ ...row, storageKind: row.storageKind as 'inline_base64' | 'file' }); } return results; } @@ -395,3 +415,93 @@ export function deleteOrphanAttachments(candidateIds: string[]): number { const result = stmt.run(...candidateIds); return result.changes; } + +// --------------------------------------------------------------------------- +// File-backed attachment operations +// --------------------------------------------------------------------------- + +/** + * Create a file-backed attachment record. The actual file content lives on + * disk at `filePath`; the DB row stores only metadata. + */ +export function createFileBackedAttachment(params: { + filename: string; + mimeType: string; + sizeBytes: number; + filePath: string; + sha256?: string; + expiresAt?: number; + thumbnailBase64?: string; +}): StoredAttachment { + const db = getDb(); + const now = Date.now(); + const kind = classifyKind(params.mimeType); + const id = uuid(); + + const record = { + id, + originalFilename: params.filename, + mimeType: params.mimeType, + sizeBytes: params.sizeBytes, + kind, + dataBase64: '', + storageKind: 'file' as const, + filePath: params.filePath, + sha256: params.sha256 ?? null, + expiresAt: params.expiresAt ?? null, + thumbnailBase64: params.thumbnailBase64 ?? null, + createdAt: now, + }; + + db.insert(attachments).values(record).run(); + + return { + id, + originalFilename: params.filename, + mimeType: params.mimeType, + sizeBytes: params.sizeBytes, + kind, + thumbnailBase64: params.thumbnailBase64 ?? null, + storageKind: 'file', + filePath: params.filePath, + sha256: params.sha256 ?? null, + expiresAt: params.expiresAt ?? null, + createdAt: now, + }; +} + +/** + * Return file-backed attachments whose retention period has elapsed. + */ +export function getExpiredFileAttachments(): Array<{ id: string; filePath: string }> { + const db = getDb(); + const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client; + const now = Date.now(); + const rows = raw + .prepare( + `SELECT id, file_path FROM attachments WHERE storage_kind = 'file' AND expires_at IS NOT NULL AND expires_at < ?`, + ) + .all(now) as Array<{ id: string; file_path: string }>; + return rows.map((r) => ({ id: r.id, filePath: r.file_path })); +} + +/** + * Delete a file-backed attachment's DB row. The caller is responsible for + * removing the file on disk. + */ +export function deleteFileBackedAttachment(attachmentId: string): 'deleted' | 'not_found' { + const db = getDb(); + const existing = db + .select({ id: attachments.id }) + .from(attachments) + .where(eq(attachments.id, attachmentId)) + .get(); + + if (!existing) return 'not_found'; + + db.delete(attachments) + .where(eq(attachments.id, attachmentId)) + .run(); + + return 'deleted'; +} diff --git a/assistant/src/memory/db-init.ts b/assistant/src/memory/db-init.ts index 90e249c767a..488ff2266ca 100644 --- a/assistant/src/memory/db-init.ts +++ b/assistant/src/memory/db-init.ts @@ -526,6 +526,10 @@ export function initializeDb(): void { try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN thread_type TEXT NOT NULL DEFAULT 'standard'`); } catch { /* already exists */ } try { database.run(/*sql*/ `ALTER TABLE conversations ADD COLUMN memory_scope_id TEXT NOT NULL DEFAULT 'default'`); } catch { /* already exists */ } try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN thumbnail_base64 TEXT`); } catch { /* already exists */ } + try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN storage_kind TEXT NOT NULL DEFAULT 'inline_base64'`); } catch { /* already exists */ } + try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN file_path TEXT`); } catch { /* already exists */ } + try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN sha256 TEXT`); } catch { /* already exists */ } + try { database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN expires_at INTEGER`); } catch { /* already exists */ } try { database.run(/*sql*/ `ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`); } catch { /* already exists */ } try { database.run(/*sql*/ `ALTER TABLE messages ADD COLUMN metadata TEXT`); } catch { /* already exists */ } try { database.run(/*sql*/ `ALTER TABLE memory_embeddings ADD COLUMN content_hash TEXT`); } catch { /* already exists */ } diff --git a/assistant/src/memory/schema.ts b/assistant/src/memory/schema.ts index 3e536bd1035..b8926f7e825 100644 --- a/assistant/src/memory/schema.ts +++ b/assistant/src/memory/schema.ts @@ -165,6 +165,10 @@ export const attachments = sqliteTable('attachments', { dataBase64: text('data_base64').notNull(), contentHash: text('content_hash'), thumbnailBase64: text('thumbnail_base64'), + storageKind: text('storage_kind').notNull().default('inline_base64'), + filePath: text('file_path'), + sha256: text('sha256'), + expiresAt: integer('expires_at'), createdAt: integer('created_at').notNull(), }); diff --git a/assistant/src/runtime/http-server.ts b/assistant/src/runtime/http-server.ts index 6b470f92365..9cbcf0db50e 100644 --- a/assistant/src/runtime/http-server.ts +++ b/assistant/src/runtime/http-server.ts @@ -27,6 +27,7 @@ import { handleUploadAttachment, handleDeleteAttachment, handleGetAttachment, + handleGetAttachmentContent, } from './routes/attachment-routes.js'; import { handleCreateRun, @@ -689,6 +690,12 @@ export class RuntimeHttpServer { return await handleDeleteAttachment(req); } + // Match attachments/:attachmentId/content — must come before the generic attachments/:attachmentId + const attachmentContentMatch = endpoint.match(/^attachments\/([^/]+)\/content$/); + if (attachmentContentMatch && req.method === 'GET') { + return handleGetAttachmentContent(attachmentContentMatch[1], req); + } + // Match attachments/:attachmentId const attachmentMatch = endpoint.match(/^attachments\/([^/]+)$/); if (attachmentMatch && req.method === 'GET') { diff --git a/assistant/src/runtime/routes/attachment-routes.ts b/assistant/src/runtime/routes/attachment-routes.ts index 2471c36b9ca..88c90723fbe 100644 --- a/assistant/src/runtime/routes/attachment-routes.ts +++ b/assistant/src/runtime/routes/attachment-routes.ts @@ -1,6 +1,7 @@ /** * Route handlers for attachment upload, download, and deletion. */ +import { existsSync, statSync } from 'node:fs'; import * as attachmentsStore from '../../memory/attachments-store.js'; import { validateAttachmentUpload, AttachmentUploadError } from '../../memory/attachments-store.js'; @@ -131,3 +132,101 @@ export function handleGetAttachment(attachmentId: string): Response { data: attachment.dataBase64, }); } + +/** + * Stream attachment content as binary. Supports Range requests for video seek. + * + * For file-backed attachments: reads from disk with optional partial content. + * For inline_base64 attachments: decodes the base64 data and returns it. + */ +export function handleGetAttachmentContent(attachmentId: string, req: Request): Response { + const attachment = attachmentsStore.getAttachmentById(attachmentId); + if (!attachment) { + return Response.json({ error: 'Attachment not found' }, { status: 404 }); + } + + if (attachment.storageKind === 'file') { + return handleFileContent(attachment, req); + } + + // inline_base64 fallback — decode and return full content + const buffer = Buffer.from(attachment.dataBase64, 'base64'); + return new Response(buffer, { + status: 200, + headers: { + 'Content-Type': attachment.mimeType, + 'Content-Length': String(buffer.length), + 'Accept-Ranges': 'bytes', + 'Content-Disposition': 'inline', + }, + }); +} + +/** + * Serve file-backed attachment content with Range header support. + */ +function handleFileContent( + attachment: attachmentsStore.StoredAttachment & { dataBase64: string }, + req: Request, +): Response { + const filePath = attachment.filePath; + if (!filePath || !existsSync(filePath)) { + return Response.json({ error: 'Attachment file not found on disk' }, { status: 404 }); + } + + let fileSize: number; + try { + fileSize = statSync(filePath).size; + } catch { + return Response.json({ error: 'Failed to read attachment file' }, { status: 500 }); + } + + const rangeHeader = req.headers.get('range'); + if (!rangeHeader) { + // Full file response + const file = Bun.file(filePath); + return new Response(file, { + status: 200, + headers: { + 'Content-Type': attachment.mimeType, + 'Content-Length': String(fileSize), + 'Accept-Ranges': 'bytes', + 'Content-Disposition': 'inline', + }, + }); + } + + // Parse Range header (only supports single byte ranges) + const rangeMatch = rangeHeader.match(/^bytes=(\d+)-(\d*)$/); + if (!rangeMatch) { + return new Response('Invalid Range header', { + status: 416, + headers: { 'Content-Range': `bytes */${fileSize}` }, + }); + } + + const start = parseInt(rangeMatch[1], 10); + const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : fileSize - 1; + + if (start >= fileSize || end >= fileSize || start > end) { + return new Response('Range not satisfiable', { + status: 416, + headers: { 'Content-Range': `bytes */${fileSize}` }, + }); + } + + const contentLength = end - start + 1; + const file = Bun.file(filePath); + const slice = file.slice(start, end + 1); + + return new Response(slice, { + status: 206, + headers: { + 'Content-Type': attachment.mimeType, + 'Content-Length': String(contentLength), + 'Content-Range': `bytes ${start}-${end}/${fileSize}`, + 'Accept-Ranges': 'bytes', + 'Content-Disposition': 'inline', + }, + }); +} diff --git a/assistant/src/tools/assets/search.ts b/assistant/src/tools/assets/search.ts index 16cf91d20dc..e3e2a02ac7a 100644 --- a/assistant/src/tools/assets/search.ts +++ b/assistant/src/tools/assets/search.ts @@ -183,7 +183,7 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[] } const limit = Math.min(params.limit ?? DEFAULT_LIMIT, MAX_RESULTS); const stmt = raw.prepare( - `SELECT a.id, a.original_filename, a.mime_type, a.size_bytes, a.kind, a.thumbnail_base64, a.created_at + `SELECT a.id, a.original_filename, a.mime_type, a.size_bytes, a.kind, a.thumbnail_base64, a.storage_kind, a.file_path, a.sha256, a.expires_at, a.created_at FROM attachments a WHERE ${whereParts.join(' AND ')} ORDER BY a.created_at DESC @@ -197,6 +197,10 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[] size_bytes: number; kind: string; thumbnail_base64: string | null; + storage_kind: string; + file_path: string | null; + sha256: string | null; + expires_at: number | null; created_at: number; }>; @@ -207,6 +211,10 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[] sizeBytes: r.size_bytes, kind: r.kind, thumbnailBase64: r.thumbnail_base64, + storageKind: r.storage_kind as 'inline_base64' | 'file', + filePath: r.file_path, + sha256: r.sha256, + expiresAt: r.expires_at, createdAt: r.created_at, })); } @@ -223,16 +231,25 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[] sizeBytes: attachments.sizeBytes, kind: attachments.kind, thumbnailBase64: attachments.thumbnailBase64, + storageKind: attachments.storageKind, + filePath: attachments.filePath, + sha256: attachments.sha256, + expiresAt: attachments.expiresAt, createdAt: attachments.createdAt, }) .from(attachments) .orderBy(desc(attachments.createdAt)) .limit(limit); + const castRow = (r: { storageKind: string } & Omit): StoredAttachment => ({ + ...r, + storageKind: r.storageKind as 'inline_base64' | 'file', + }); + if (where) { - return query.where(where).all(); + return query.where(where).all().map(castRow); } - return query.all(); + return query.all().map(castRow); } // --------------------------------------------------------------------------- From 6de8215ff51e293de957e107ea80a6c531d9b878 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 10:58:01 -0500 Subject: [PATCH 05/72] fix: preserve CU metadata until finalization, fix latencyMs type in IPCMessages (#6912) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/handlers/computer-use.ts | 24 ++++++++++++++++++- clients/shared/IPC/IPCMessages.swift | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index be48ce9d72f..a0d648f49f7 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -26,7 +26,11 @@ function removeCuSessionReferences( return; } ctx.cuSessions.delete(sessionId); - ctx.cuSessionMetadata.delete(sessionId); + // NOTE: cuSessionMetadata is intentionally NOT deleted here. + // onTerminal fires before cu_session_finalized arrives from the client, + // so deleting metadata here would race with handleCuSessionFinalized + // which still needs to read it. Metadata is cleaned up explicitly at the + // end of handleCuSessionFinalized instead. cuObservationSequenceBySession.delete(sessionId); ctx.cuObservationParseSequence.delete(sessionId); for (const [sock, ids] of ctx.socketToCuSession) { @@ -48,6 +52,9 @@ export function handleCuSessionCreate( if (existingSession) { existingSession.abort(); removeCuSessionReferences(ctx, msg.sessionId, existingSession); + // Clean up stale metadata from the replaced session; the new session + // will set its own metadata below if needed. + ctx.cuSessionMetadata.delete(msg.sessionId); } const config = getConfig(); @@ -113,6 +120,8 @@ export function handleCuSessionAbort( } session.abort(); removeCuSessionReferences(ctx, msg.sessionId, session); + // On explicit abort, clean up metadata too — no finalized event is guaranteed. + ctx.cuSessionMetadata.delete(msg.sessionId); log.info({ sessionId: msg.sessionId }, 'Computer-use session aborted by client'); } @@ -249,6 +258,16 @@ export function handleCuSessionFinalized( ...(msg.recording ? { recordingPath: msg.recording.localPath } : {}), }); + // Also append to the in-memory Session.messages so subsequent turns + // in the same session see the injected summary without a reload. + const activeSession = ctx.sessions.get(reportSessionId); + if (activeSession) { + activeSession.messages.push({ + role: 'assistant', + content: [{ type: 'text', text: msg.summary }], + }); + } + // If the reporting session has a connected client, stream the summary // so it appears in real time. if (reportSocket) { @@ -277,6 +296,9 @@ export function handleCuSessionFinalized( // Clean up all CU session state. removeCuSessionReferences(ctx, msg.sessionId); + // Delete metadata last — after it has been consumed for summary injection + // above and after removeCuSessionReferences (which intentionally skips it). + ctx.cuSessionMetadata.delete(msg.sessionId); } export const computerUseHandlers = defineHandlers({ diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index ae8dcd9e188..0e9f76533f0 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -762,7 +762,7 @@ extension IPCMemoryRecalled { selectedCount: Int, rerankApplied: Bool, injectedTokens: Int, - latencyMs: Double, + latencyMs: Int, topCandidates: [IPCMemoryRecalledCandidateDebug] ) { self.init( From 7b4b32dc186c50f7e9c7670f247e73b3ce1394a0 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:15:22 -0500 Subject: [PATCH 06/72] fix: preserve new columns in legacy migration and clamp range ends per RFC 7233 (#6913) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../attachment-content-route.test.ts | 28 ++++++++++++++++++- assistant/src/memory/schema-migration.ts | 12 ++++++-- .../src/runtime/routes/attachment-routes.ts | 4 +-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/assistant/src/__tests__/attachment-content-route.test.ts b/assistant/src/__tests__/attachment-content-route.test.ts index 57168a19116..48d65211559 100644 --- a/assistant/src/__tests__/attachment-content-route.test.ts +++ b/assistant/src/__tests__/attachment-content-route.test.ts @@ -136,7 +136,7 @@ describe('handleGetAttachmentContent — file-backed', () => { expect(Buffer.from(body).toString()).toBe('fghij'); }); - test('returns 416 for unsatisfiable range', () => { + test('returns 416 when start is beyond file size', () => { const filePath = join(testDir, 'test-416.mp4'); const content = Buffer.from('short'); writeFileSync(filePath, content); @@ -156,6 +156,32 @@ describe('handleGetAttachmentContent — file-backed', () => { expect(res.status).toBe(416); }); + test('clamps oversized end to fileSize-1 per RFC 7233', async () => { + const filePath = join(testDir, 'test-clamp.mp4'); + const content = Buffer.from('0123456789'); + writeFileSync(filePath, content); + + const attachment = createFileBackedAttachment({ + filename: 'test-clamp.mp4', + mimeType: 'video/mp4', + sizeBytes: content.length, + filePath, + }); + + // Request bytes=5-999 on a 10-byte file; end should be clamped to 9 + const req = new Request('http://localhost/v1/attachments/' + attachment.id + '/content', { + headers: { Range: 'bytes=5-999' }, + }); + const res = handleGetAttachmentContent(attachment.id, req); + + expect(res.status).toBe(206); + expect(res.headers.get('Content-Range')).toBe(`bytes 5-9/${content.length}`); + expect(res.headers.get('Content-Length')).toBe('5'); + + const body = await res.arrayBuffer(); + expect(Buffer.from(body).toString()).toBe('56789'); + }); + test('returns 404 when file is missing from disk', () => { const attachment = createFileBackedAttachment({ filename: 'missing.mp4', diff --git a/assistant/src/memory/schema-migration.ts b/assistant/src/memory/schema-migration.ts index b92550718e7..5b1f6e6cbba 100644 --- a/assistant/src/memory/schema-migration.ts +++ b/assistant/src/memory/schema-migration.ts @@ -646,12 +646,18 @@ export function migrateRemoveAssistantIdColumns(database: Db): void { data_base64 TEXT NOT NULL, content_hash TEXT, thumbnail_base64 TEXT, - created_at INTEGER NOT NULL + created_at INTEGER NOT NULL, + storage_kind TEXT DEFAULT 'inline_base64', + file_path TEXT, + sha256 TEXT, + expires_at INTEGER ) `); raw.exec(/*sql*/ ` - INSERT INTO attachments_new (id, original_filename, mime_type, size_bytes, kind, data_base64, content_hash, thumbnail_base64, created_at) - SELECT id, original_filename, mime_type, size_bytes, kind, data_base64, content_hash, thumbnail_base64, created_at FROM attachments + INSERT INTO attachments_new (id, original_filename, mime_type, size_bytes, kind, data_base64, content_hash, thumbnail_base64, created_at, storage_kind, file_path, sha256, expires_at) + SELECT id, original_filename, mime_type, size_bytes, kind, data_base64, content_hash, thumbnail_base64, created_at, + COALESCE(storage_kind, 'inline_base64'), file_path, sha256, expires_at + FROM attachments `); raw.exec(/*sql*/ `DROP TABLE attachments`); raw.exec(/*sql*/ `ALTER TABLE attachments_new RENAME TO attachments`); diff --git a/assistant/src/runtime/routes/attachment-routes.ts b/assistant/src/runtime/routes/attachment-routes.ts index 88c90723fbe..4b65805cf0a 100644 --- a/assistant/src/runtime/routes/attachment-routes.ts +++ b/assistant/src/runtime/routes/attachment-routes.ts @@ -206,9 +206,9 @@ function handleFileContent( } const start = parseInt(rangeMatch[1], 10); - const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : fileSize - 1; + const end = rangeMatch[2] ? Math.min(parseInt(rangeMatch[2], 10), fileSize - 1) : fileSize - 1; - if (start >= fileSize || end >= fileSize || start > end) { + if (start >= fileSize || start > end) { return new Response('Range not satisfiable', { status: 416, headers: { 'Content-Range': `bytes */${fileSize}` }, From e02dbe81f95f5be3d794a5768083f0f9ebdf7214 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:19:10 -0500 Subject: [PATCH 07/72] feat: add macOS screen recorder, CU session QA integration, and video drag support (#6916) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../ComputerUse/ScreenRecorder.swift | 370 ++++++++++++++++++ .../ComputerUse/Session.swift | 108 ++++- .../InlineVideoAttachmentView.swift | 53 +++ clients/shared/IPC/IPCMessages.swift | 16 + 4 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift diff --git a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift new file mode 100644 index 00000000000..a2d380adaca --- /dev/null +++ b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift @@ -0,0 +1,370 @@ +import Foundation +import ScreenCaptureKit +import AVFoundation +import CoreMedia +import os + +private let log = Logger(subsystem: "com.vellum.vellum-assistant", category: "ScreenRecorder") + +// MARK: - Recording Result + +/// Metadata returned after a screen recording session completes. +struct RecordingResult: Sendable { + let fileURL: URL + let mimeType: String // always "video/mp4" + let sizeBytes: Int + let durationMs: Int + let width: Int + let height: Int + let captureScope: String // "window" or "display" + let includeAudio: Bool + let targetBundleId: String? +} + +// MARK: - Recording Errors + +enum ScreenRecorderError: LocalizedError { + case alreadyRecording + case notRecording + case permissionDenied + case noDisplayFound + case windowNotFound(CGWindowID) + case assetWriterSetupFailed(String) + case assetWriterFailed(String) + case recordingDirectoryCreationFailed + + var errorDescription: String? { + switch self { + case .alreadyRecording: + return "Screen recording is already in progress" + case .notRecording: + return "No active screen recording to stop" + case .permissionDenied: + return "Screen Recording permission denied. Grant it in System Settings > Privacy & Security > Screen Recording." + case .noDisplayFound: + return "No display found for recording" + case .windowNotFound(let id): + return "Window with ID \(id) not found for recording" + case .assetWriterSetupFailed(let reason): + return "Failed to set up recording writer: \(reason)" + case .assetWriterFailed(let reason): + return "Recording writer error: \(reason)" + case .recordingDirectoryCreationFailed: + return "Failed to create recordings directory" + } + } +} + +// MARK: - Protocol + +/// Protocol for screen recording, enabling dependency injection and testing. +@MainActor +protocol ScreenRecording { + func startRecording(windowID: CGWindowID?, displayID: CGDirectDisplayID?, includeAudio: Bool) async throws + func stopRecording() async throws -> RecordingResult + var isRecording: Bool { get } +} + +// MARK: - ScreenRecorder + +/// Records screen content to an .mp4 file using ScreenCaptureKit (SCStream). +/// +/// Supports two capture scopes: +/// - **Window capture**: captures a specific window by CGWindowID +/// - **Display capture**: captures the full display (fallback) +/// +/// Recordings are saved to `~/Library/Application Support/vellum-assistant/recordings/`. +@MainActor +final class ScreenRecorder: NSObject, ScreenRecording { + private(set) var isRecording = false + + private var stream: SCStream? + private var assetWriter: AVAssetWriter? + private var videoInput: AVAssetWriterInput? + private var audioInput: AVAssetWriterInput? + private var recordingFileURL: URL? + private var recordingStartTime: Date? + private var captureScope: String = "display" + private var includesAudio: Bool = false + private var targetBundleId: String? + private var captureWidth: Int = 0 + private var captureHeight: Int = 0 + + /// Nonisolated delegate that buffers samples and forwards them to the asset writer. + /// Must be nonisolated because SCStreamOutput callbacks arrive on an arbitrary queue. + private var outputHandler: StreamOutputHandler? + + // MARK: - Directory Setup + + private static func recordingsDirectory() throws -> URL { + let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let recordingsDir = appSupport + .appendingPathComponent("vellum-assistant", isDirectory: true) + .appendingPathComponent("recordings", isDirectory: true) + + if !FileManager.default.fileExists(atPath: recordingsDir.path) { + do { + try FileManager.default.createDirectory(at: recordingsDir, withIntermediateDirectories: true) + } catch { + log.error("Failed to create recordings directory: \(error.localizedDescription)") + throw ScreenRecorderError.recordingDirectoryCreationFailed + } + } + + return recordingsDir + } + + // MARK: - Start Recording + + func startRecording(windowID: CGWindowID? = nil, displayID: CGDirectDisplayID? = nil, includeAudio: Bool = false) async throws { + guard !isRecording else { + throw ScreenRecorderError.alreadyRecording + } + + // Fetch shareable content (triggers permission prompt if needed) + let content: SCShareableContent + do { + content = try await SCShareableContent.current + } catch { + throw ScreenRecorderError.permissionDenied + } + + // Build content filter based on capture scope + let filter: SCContentFilter + if let windowID, let window = content.windows.first(where: { $0.windowID == windowID }) { + filter = SCContentFilter(desktopIndependentWindow: window) + captureScope = "window" + targetBundleId = window.owningApplication?.bundleIdentifier + log.info("Recording window \(windowID) (bundle: \(self.targetBundleId ?? "unknown"))") + } else if let displayID, let display = content.displays.first(where: { $0.displayID == displayID }) { + // Exclude our own app's windows from display capture + let myPID = ProcessInfo.processInfo.processIdentifier + let ownWindows = content.windows.filter { $0.owningApplication?.processID == myPID } + filter = SCContentFilter(display: display, excludingWindows: ownWindows) + captureScope = "display" + targetBundleId = nil + log.info("Recording display \(displayID)") + } else { + // Fallback: use main display + let mainDisplayID = CGMainDisplayID() + guard let display = content.displays.first(where: { $0.displayID == mainDisplayID }) + ?? content.displays.first else { + throw ScreenRecorderError.noDisplayFound + } + let myPID = ProcessInfo.processInfo.processIdentifier + let ownWindows = content.windows.filter { $0.owningApplication?.processID == myPID } + filter = SCContentFilter(display: display, excludingWindows: ownWindows) + captureScope = "display" + targetBundleId = nil + log.info("Recording main display (fallback)") + } + + includesAudio = includeAudio + + // Configure the stream + let config = SCStreamConfiguration() + config.width = 1920 + config.height = 1080 + config.pixelFormat = kCVPixelFormatType_32BGRA + config.showsCursor = true + config.minimumFrameInterval = CMTime(value: 1, timescale: 30) // 30 fps + + if includeAudio { + config.capturesAudio = true + config.sampleRate = 44100 + config.channelCount = 2 + } + + captureWidth = config.width + captureHeight = config.height + + // Set up the output file + let recordingsDir = try Self.recordingsDirectory() + let timestamp = ISO8601DateFormatter().string(from: Date()) + .replacingOccurrences(of: ":", with: "-") + let fileName = "qa-recording-\(timestamp).mp4" + let fileURL = recordingsDir.appendingPathComponent(fileName) + recordingFileURL = fileURL + + // Set up AVAssetWriter + let writer: AVAssetWriter + do { + writer = try AVAssetWriter(outputURL: fileURL, fileType: .mp4) + } catch { + throw ScreenRecorderError.assetWriterSetupFailed(error.localizedDescription) + } + + // Video input + let videoSettings: [String: Any] = [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: config.width, + AVVideoHeightKey: config.height, + AVVideoCompressionPropertiesKey: [ + AVVideoAverageBitRateKey: 4_000_000, // 4 Mbps + AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, + ] + ] + let vInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + vInput.expectsMediaDataInRealTime = true + guard writer.canAdd(vInput) else { + throw ScreenRecorderError.assetWriterSetupFailed("Cannot add video input to asset writer") + } + writer.add(vInput) + videoInput = vInput + + // Audio input (optional) + if includeAudio { + let audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVSampleRateKey: 44100, + AVNumberOfChannelsKey: 2, + AVEncoderBitRateKey: 128_000, + ] + let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings) + aInput.expectsMediaDataInRealTime = true + if writer.canAdd(aInput) { + writer.add(aInput) + audioInput = aInput + } + } + + writer.startWriting() + assetWriter = writer + + // Create the nonisolated output handler + let handler = StreamOutputHandler(writer: writer, videoInput: vInput, audioInput: audioInput) + outputHandler = handler + + // Create and start the stream + let scStream = SCStream(filter: filter, configuration: config, delegate: nil) + try scStream.addStreamOutput(handler, type: .screen, sampleHandlerQueue: .global(qos: .userInitiated)) + if includeAudio { + try scStream.addStreamOutput(handler, type: .audio, sampleHandlerQueue: .global(qos: .userInitiated)) + } + + try await scStream.startCapture() + stream = scStream + isRecording = true + recordingStartTime = Date() + + log.info("Screen recording started: \(fileURL.lastPathComponent)") + } + + // MARK: - Stop Recording + + func stopRecording() async throws -> RecordingResult { + guard isRecording, let stream, let writer = assetWriter, let fileURL = recordingFileURL else { + throw ScreenRecorderError.notRecording + } + + // Stop the stream capture + do { + try await stream.stopCapture() + } catch { + log.warning("Error stopping stream capture: \(error.localizedDescription)") + } + + // Mark inputs as finished + videoInput?.markAsFinished() + audioInput?.markAsFinished() + + // Finalize the asset writer + await writer.finishWriting() + + if writer.status == .failed { + let errorMsg = writer.error?.localizedDescription ?? "Unknown error" + log.error("Asset writer failed: \(errorMsg)") + throw ScreenRecorderError.assetWriterFailed(errorMsg) + } + + // Compute metadata + let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + let sizeBytes = (fileAttributes[.size] as? Int) ?? 0 + + // Compute duration from the asset + let asset = AVAsset(url: fileURL) + let duration: CMTime + if let tracks = try? await asset.load(.tracks), !tracks.isEmpty { + duration = try await asset.load(.duration) + } else { + // Fallback: estimate from wall clock time + let elapsed = recordingStartTime.map { Date().timeIntervalSince($0) } ?? 0 + duration = CMTime(seconds: elapsed, preferredTimescale: 1000) + } + let durationMs = Int(CMTimeGetSeconds(duration) * 1000) + + let result = RecordingResult( + fileURL: fileURL, + mimeType: "video/mp4", + sizeBytes: sizeBytes, + durationMs: durationMs, + width: captureWidth, + height: captureHeight, + captureScope: captureScope, + includeAudio: includesAudio, + targetBundleId: targetBundleId + ) + + // Clean up state + self.stream = nil + self.assetWriter = nil + self.videoInput = nil + self.audioInput = nil + self.outputHandler = nil + self.recordingFileURL = nil + self.recordingStartTime = nil + self.isRecording = false + + log.info("Screen recording stopped: \(fileURL.lastPathComponent) (\(sizeBytes) bytes, \(durationMs)ms)") + + return result + } +} + +// MARK: - Stream Output Handler + +/// Nonisolated handler for SCStream output that writes samples to an AVAssetWriter. +/// SCStreamOutput callbacks arrive on arbitrary queues, so this class must not be +/// @MainActor-isolated. +private final class StreamOutputHandler: NSObject, SCStreamOutput, @unchecked Sendable { + private let writer: AVAssetWriter + private let videoInput: AVAssetWriterInput + private let audioInput: AVAssetWriterInput? + private var sessionStarted = false + private let lock = NSLock() + + init(writer: AVAssetWriter, videoInput: AVAssetWriterInput, audioInput: AVAssetWriterInput?) { + self.writer = writer + self.videoInput = videoInput + self.audioInput = audioInput + super.init() + } + + func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) { + guard writer.status == .writing else { return } + guard sampleBuffer.isValid else { return } + + lock.lock() + defer { lock.unlock() } + + // Start the session on the first sample + if !sessionStarted { + let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + writer.startSession(atSourceTime: timestamp) + sessionStarted = true + } + + switch type { + case .screen: + if videoInput.isReadyForMoreMediaData { + videoInput.append(sampleBuffer) + } + case .audio: + if let audioInput, audioInput.isReadyForMoreMediaData { + audioInput.append(sampleBuffer) + } + @unknown default: + break + } + } +} diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index e21dfc64743..f76cbb971c3 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -34,6 +34,13 @@ final class ComputerUseSession: ObservableObject { private let skipSessionCreate: Bool private let notificationService: ActivityNotificationServiceProtocol? + /// Screen recorder for QA mode — nil when not in QA mode. + private let screenRecorder: ScreenRecording? + /// Origin chat session ID for result injection (QA workflow). + let reportToSessionId: String? + /// Whether this session is running in QA/test mode. + let qaMode: Bool + /// Weak reference to the chat view model for extracting tool calls for notifications. weak var relatedViewModel: ChatViewModel? @@ -74,7 +81,10 @@ final class ComputerUseSession: ObservableObject { adaptiveDelay: Bool = true, sessionId: String? = nil, skipSessionCreate: Bool = false, - notificationService: ActivityNotificationServiceProtocol? = nil + notificationService: ActivityNotificationServiceProtocol? = nil, + screenRecorder: ScreenRecording? = nil, + reportToSessionId: String? = nil, + qaMode: Bool = false ) { self.id = sessionId ?? UUID().uuidString self.task = task @@ -89,6 +99,9 @@ final class ComputerUseSession: ObservableObject { self.adaptiveDelayEnabled = adaptiveDelay self.skipSessionCreate = skipSessionCreate self.notificationService = notificationService + self.screenRecorder = screenRecorder + self.reportToSessionId = reportToSessionId + self.qaMode = qaMode self.verifier = ActionVerifier(maxSteps: maxSteps) self.logger = SessionLogger(task: task, attachments: attachments) } @@ -114,6 +127,17 @@ final class ComputerUseSession: ObservableObject { try? await Task.sleep(nanoseconds: initialDelayMs * 1_000_000) } + // Start screen recording in QA mode + if qaMode, let recorder = screenRecorder { + do { + try await recorder.startRecording(windowID: nil, displayID: nil, includeAudio: false) + log.info("QA mode: screen recording started for session \(self.id)") + } catch { + log.error("QA mode: failed to start screen recording: \(error.localizedDescription)") + // Non-fatal — continue the session without recording + } + } + // 1. Subscribe before sending so we don't miss fast daemon responses let messageStream = daemonClient.subscribe() @@ -138,7 +162,9 @@ final class ComputerUseSession: ObservableObject { screenWidth: Int(screenSize.width), screenHeight: Int(screenSize.height), attachments: ipcAttachments, - interactionType: interactionTypeString + interactionType: interactionTypeString, + reportToSessionId: reportToSessionId, + qaMode: qaMode ? true : nil )) } catch { log.error("Failed to send session create message: \(error)") @@ -161,6 +187,9 @@ final class ComputerUseSession: ObservableObject { log.error("Failed to send session abort message: \(error)") } logger.finishSession(result: "failed: no window") + if qaMode { + await finalizeQARecording() + } return } @@ -243,6 +272,11 @@ final class ComputerUseSession: ObservableObject { logger.finishSession(result: "failed: stream ended unexpectedly") } } + + // Finalize QA recording and send cu_session_finalized + if qaMode { + await finalizeQARecording() + } } // MARK: - Action Handler @@ -910,6 +944,76 @@ final class ComputerUseSession: ObservableObject { .flatMap { $0.toolCalls } } + // MARK: - QA Recording Finalization + + /// Stops the screen recorder (if active) and sends a `cu_session_finalized` message to the daemon. + private func finalizeQARecording() async { + // Map SessionState to a status string + let status: String + let summary: String + let stepCount: Int + switch state { + case .completed(let s, let steps): + status = "completed" + summary = s + stepCount = steps + case .responded(let answer, let steps): + status = "responded" + summary = answer + stepCount = steps + case .failed(let reason): + status = "failed" + summary = reason + stepCount = currentStepNumber + case .cancelled: + status = "cancelled" + summary = "Session cancelled by user" + stepCount = currentStepNumber + default: + status = "unknown" + summary = "Session ended in unexpected state" + stepCount = currentStepNumber + } + + // Stop the recorder and gather metadata + var recordingData: IPCCuSessionFinalizedRecording? + if let recorder = screenRecorder, recorder.isRecording { + do { + let result = try await recorder.stopRecording() + let expiresAtEpoch = Int(Date().addingTimeInterval(7 * 24 * 3600).timeIntervalSince1970) + recordingData = IPCCuSessionFinalizedRecording( + localPath: result.fileURL.path, + mimeType: result.mimeType, + sizeBytes: result.sizeBytes, + durationMs: result.durationMs, + width: result.width, + height: result.height, + captureScope: result.captureScope, + includeAudio: result.includeAudio, + targetBundleId: result.targetBundleId, + expiresAt: expiresAtEpoch + ) + log.info("QA recording finalized: \(result.fileURL.lastPathComponent) (\(result.sizeBytes) bytes, \(result.durationMs)ms)") + } catch { + log.error("QA mode: failed to stop screen recording: \(error.localizedDescription)") + } + } + + // Send cu_session_finalized to the daemon + do { + try daemonClient.send(CuSessionFinalizedMessage( + sessionId: id, + status: status, + summary: summary, + stepCount: stepCount, + recording: recordingData + )) + log.info("QA mode: sent cu_session_finalized for session \(self.id) (status: \(status))") + } catch { + log.error("QA mode: failed to send cu_session_finalized: \(error.localizedDescription)") + } + } + // MARK: - Control func pause() { diff --git a/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift b/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift index f2ed4bf5f34..5a514888175 100644 --- a/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift @@ -87,6 +87,9 @@ struct InlineVideoAttachmentView: View { .frame(maxWidth: 360) .aspectRatio(videoAspectRatio, contentMode: .fit) .onHover { isHovering = $0 } + .onDrag { + dragItemProvider() + } .onDisappear { player?.pause() player = nil @@ -339,6 +342,56 @@ struct InlineVideoAttachmentView: View { } } + /// Creates an NSItemProvider for drag-and-drop to Finder or other apps. + /// Uses the cached temp file if available, otherwise writes inline data to disk first. + private func dragItemProvider() -> NSItemProvider { + if let fileURL = cachedFileURL { + return NSItemProvider(contentsOf: fileURL) ?? NSItemProvider() + } + + // Write inline base64 data to a temp file for dragging + if !attachment.data.isEmpty, let data = Data(base64Encoded: attachment.data) { + let fileURL = safeTempURL() + do { + try data.write(to: fileURL) + return NSItemProvider(contentsOf: fileURL) ?? NSItemProvider() + } catch { + log.warning("Failed to write video for drag: \(error.localizedDescription)") + } + } + + // Fallback: provide the filename as a promise (lazy-loaded attachments) + if attachment.isLazyLoad, let port = daemonHttpPort, !attachment.id.isEmpty { + let provider = NSItemProvider() + let fileURL = safeTempURL() + let attachmentId = attachment.id + provider.suggestedName = (attachment.filename as NSString).lastPathComponent + provider.registerFileRepresentation( + forTypeIdentifier: "public.mpeg-4", + fileOptions: [], + visibility: .all + ) { completion in + Task { + do { + let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId) + guard let data = Data(base64Encoded: base64) else { + completion(nil, false, URLError(.cannotDecodeContentData)) + return + } + try data.write(to: fileURL) + completion(fileURL, true, nil) + } catch { + completion(nil, false, error) + } + } + return nil + } + return provider + } + + return NSItemProvider() + } + private func openInExternalPlayer() { if let fileURL = cachedFileURL { NSWorkspace.shared.open(fileURL) diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index 0e9f76533f0..c960a7a0f00 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -153,6 +153,22 @@ extension IPCCuSessionCreate { } } +/// Sent when a CU session reaches a terminal state (QA mode). +/// Backed by generated `IPCCuSessionFinalized`. +public typealias CuSessionFinalizedMessage = IPCCuSessionFinalized + +extension IPCCuSessionFinalized { + public init(sessionId: String, status: String, summary: String, stepCount: Int, recording: IPCCuSessionFinalizedRecording?) { + self.init(type: "cu_session_finalized", sessionId: sessionId, status: status, summary: summary, stepCount: stepCount, recording: recording) + } +} + +extension IPCCuSessionFinalizedRecording { + public init(localPath: String, mimeType: String, sizeBytes: Int, durationMs: Int, width: Int, height: Int, captureScope: String, includeAudio: Bool, targetBundleId: String?, expiresAt: Int) { + self.init(localPath: localPath, mimeType: mimeType, sizeBytes: sizeBytes, durationMs: durationMs, width: width, height: height, captureScope: captureScope, includeAudio: includeAudio, targetBundleId: targetBundleId, expiresAt: expiresAt) + } +} + /// Sent after each perceive step with AX tree, screenshot, and execution results. /// Backed by generated `IPCCuObservation`. public typealias CuObservationMessage = IPCCuObservation From f955aa4397fd9c65a7ae4b6d4483f90895226df7 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:27:15 -0500 Subject: [PATCH 08/72] feat: add retention cleanup, config, tests, and architecture docs for QA video (#6919) Co-authored-by: Vellum Assistant --- ARCHITECTURE.md | 48 +++- .../src/__tests__/recording-cleanup.test.ts | 237 ++++++++++++++++++ assistant/src/config/defaults.ts | 4 + assistant/src/config/schema.ts | 18 ++ assistant/src/config/types.ts | 1 + assistant/src/daemon/lifecycle.ts | 5 + assistant/src/daemon/recording-cleanup.ts | 100 ++++++++ 7 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 assistant/src/__tests__/recording-cleanup.test.ts create mode 100644 assistant/src/daemon/recording-cleanup.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ff6372141b5..821a6a4538c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4253,6 +4253,51 @@ Keep-alive heartbeats (every 30 s by default): --- +## QA Recording — Automated Video Capture and Retention + +### QA Recording Data Flow + +``` +User asks to test → QA intent detection → CU session created with qaMode + reportToSessionId +→ macOS ScreenRecorder starts → CU action loop executes +→ Session terminates → ScreenRecorder stops → .mp4 saved to ~/Library/Application Support/vellum-assistant/recordings/ +→ cu_session_finalized sent to daemon with recording metadata +→ Daemon handler creates file-backed attachment + assistant message in source chat +→ Client loads video from GET /v1/attachments/:id/content (with Range support) +→ Video playable inline + draggable to Finder +→ Retention cleanup removes expired recordings after configurable period (default 7 days) +``` + +### File-Backed Attachment Storage + +The attachments table supports two storage kinds: + +| `storageKind` | Data location | Use case | +|---------------|---------------|----------| +| `inline_base64` | `dataBase64` column in SQLite | Small attachments (images, documents, up to 20 MB) | +| `file` | On-disk file referenced by `filePath` column | Large files (QA recordings, videos) | + +File-backed attachments store only metadata in SQLite (filename, MIME type, size, SHA-256 hash, expiry timestamp). Binary content is served via `GET /v1/attachments/:id/content` with HTTP Range header support for streaming video playback. + +### Retention Cleanup + +A periodic cleanup worker (`recording-cleanup.ts`) runs on a configurable interval (default: every 6 hours, set via `qaRecording.cleanupIntervalMs`). It also runs one pass on daemon startup to catch recordings that expired while the daemon was offline. + +The cleanup pass: +1. Queries `getExpiredFileAttachments()` for file-backed attachments where `expiresAt < now` +2. Deletes the underlying file from disk via `fs.unlinkSync` +3. Removes the DB row via `deleteFileBackedAttachment(id)` +4. Logs a summary of cleaned-up recordings and freed disk space + +| Key files | Purpose | +|-----------|---------| +| `assistant/src/daemon/recording-cleanup.ts` | Cleanup worker (start/stop/runPass) | +| `assistant/src/memory/attachments-store.ts` | `createFileBackedAttachment`, `getExpiredFileAttachments`, `deleteFileBackedAttachment` | +| `assistant/src/config/schema.ts` | `QaRecordingConfigSchema` (retention days, cleanup interval) | +| `assistant/src/daemon/lifecycle.ts` | Wires cleanup worker start/stop into daemon init/shutdown | + +--- + ## Storage Summary | What | Where | Format | ORM/Driver | Retention | @@ -4270,7 +4315,8 @@ Keep-alive heartbeats (every 30 s by default): | Entity graph (entities/relations/item links) | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Permanent, deduped by unique relation edge | | Embeddings | `~/.vellum/workspace/data/db/assistant.db` | JSON float arrays | Drizzle ORM | Permanent | | Async job queue | `~/.vellum/workspace/data/db/assistant.db` | SQLite | Drizzle ORM | Completed jobs persist | -| Attachments | `~/.vellum/workspace/data/db/assistant.db` | Base64 in SQLite | Drizzle ORM | Permanent | +| Attachments (inline) | `~/.vellum/workspace/data/db/assistant.db` | Base64 in SQLite | Drizzle ORM | Permanent | +| Attachments (file-backed) | `~/Library/Application Support/vellum-assistant/recordings/` + metadata in SQLite | Binary on disk, metadata in SQLite | Drizzle ORM + fs | Configurable (`qaRecording.defaultRetentionDays`, default 7 days) | | Sandbox filesystem | `~/.vellum/workspace` | Real filesystem tree | Node FS APIs | Persistent across sessions | | Tool permission rules | `~/.vellum/protected/trust.json` | JSON | File I/O | Permanent | | Web users & assistants | PostgreSQL | Relational | Drizzle ORM (pg) | Permanent | diff --git a/assistant/src/__tests__/recording-cleanup.test.ts b/assistant/src/__tests__/recording-cleanup.test.ts new file mode 100644 index 00000000000..9ee5b92ed0c --- /dev/null +++ b/assistant/src/__tests__/recording-cleanup.test.ts @@ -0,0 +1,237 @@ +import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test'; +import { mkdtempSync, rmSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const testDir = mkdtempSync(join(tmpdir(), 'recording-cleanup-test-')); + +mock.module('../util/platform.js', () => ({ + getDataDir: () => testDir, + isMacOS: () => process.platform === 'darwin', + isLinux: () => process.platform === 'linux', + isWindows: () => process.platform === 'win32', + getSocketPath: () => join(testDir, 'test.sock'), + getPidPath: () => join(testDir, 'test.pid'), + getDbPath: () => join(testDir, 'test.db'), + getLogPath: () => join(testDir, 'test.log'), + ensureDataDir: () => {}, + getRootDir: () => testDir, +})); + +mock.module('../util/logger.js', () => ({ + getLogger: () => new Proxy({} as Record, { + get: () => () => {}, + }), +})); + +mock.module('../config/loader.js', () => ({ + getConfig: () => ({ + model: 'test', + provider: 'test', + apiKeys: {}, + memory: { enabled: false }, + rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 }, + }), +})); + +import { initializeDb, getDb, resetDb } from '../memory/db.js'; +import { + uploadAttachment, + createFileBackedAttachment, + getAttachmentById, + getExpiredFileAttachments, +} from '../memory/attachments-store.js'; +import { runCleanupPass } from '../daemon/recording-cleanup.js'; + +initializeDb(); + +afterAll(() => { + resetDb(); + try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ } +}); + +function resetTables() { + const db = getDb(); + db.run('DELETE FROM message_attachments'); + db.run('DELETE FROM attachments'); +} + +// --------------------------------------------------------------------------- +// Cleanup pass tests +// --------------------------------------------------------------------------- + +describe('runCleanupPass', () => { + beforeEach(resetTables); + + test('deletes expired file-backed attachments and their files', () => { + const now = Date.now(); + const recordingsDir = join(testDir, 'recordings'); + mkdirSync(recordingsDir, { recursive: true }); + + // Create a file on disk + const filePath = join(recordingsDir, 'expired-recording.mp4'); + writeFileSync(filePath, Buffer.alloc(1024, 0)); // 1 KB dummy file + + // Create expired file-backed attachment + const expired = createFileBackedAttachment({ + filename: 'expired-recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 1024, + filePath, + expiresAt: now - 10000, // expired 10 seconds ago + }); + + // Verify file exists before cleanup + expect(existsSync(filePath)).toBe(true); + + const result = runCleanupPass(); + + expect(result.cleaned).toBe(1); + expect(result.bytesFreed).toBe(1024); + + // File should be removed from disk + expect(existsSync(filePath)).toBe(false); + + // DB row should be removed + expect(getAttachmentById(expired.id)).toBeNull(); + }); + + test('does not touch non-expired file-backed attachments', () => { + const now = Date.now(); + const recordingsDir = join(testDir, 'recordings'); + mkdirSync(recordingsDir, { recursive: true }); + + const filePath = join(recordingsDir, 'fresh-recording.mp4'); + writeFileSync(filePath, Buffer.alloc(512, 0)); + + const fresh = createFileBackedAttachment({ + filename: 'fresh-recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 512, + filePath, + expiresAt: now + 86400000, // expires tomorrow + }); + + const result = runCleanupPass(); + + expect(result.cleaned).toBe(0); + expect(result.bytesFreed).toBe(0); + + // File should still exist + expect(existsSync(filePath)).toBe(true); + + // DB row should still exist + expect(getAttachmentById(fresh.id)).not.toBeNull(); + }); + + test('never touches inline_base64 attachments', () => { + // Create an inline base64 attachment + const inline = uploadAttachment('chart.png', 'image/png', 'iVBORw0K'); + + const result = runCleanupPass(); + + expect(result.cleaned).toBe(0); + + // Inline attachment should still exist + expect(getAttachmentById(inline.id)).not.toBeNull(); + }); + + test('handles missing files gracefully (file already deleted)', () => { + const now = Date.now(); + + // Create expired attachment pointing to a non-existent file + const expired = createFileBackedAttachment({ + filename: 'ghost-recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 2048, + filePath: join(testDir, 'nonexistent', 'ghost.mp4'), + expiresAt: now - 10000, + }); + + const result = runCleanupPass(); + + // Should still clean up the DB row even if the file is missing + expect(result.cleaned).toBe(1); + expect(result.bytesFreed).toBe(0); // no file to measure + + expect(getAttachmentById(expired.id)).toBeNull(); + }); + + test('cleans up multiple expired recordings in one pass', () => { + const now = Date.now(); + const recordingsDir = join(testDir, 'recordings-multi'); + mkdirSync(recordingsDir, { recursive: true }); + + const fileA = join(recordingsDir, 'a.mp4'); + const fileB = join(recordingsDir, 'b.mp4'); + writeFileSync(fileA, Buffer.alloc(2048, 0)); + writeFileSync(fileB, Buffer.alloc(4096, 0)); + + createFileBackedAttachment({ + filename: 'a.mp4', + mimeType: 'video/mp4', + sizeBytes: 2048, + filePath: fileA, + expiresAt: now - 5000, + }); + + createFileBackedAttachment({ + filename: 'b.mp4', + mimeType: 'video/mp4', + sizeBytes: 4096, + filePath: fileB, + expiresAt: now - 3000, + }); + + // Also add a non-expired one + const fileC = join(recordingsDir, 'c.mp4'); + writeFileSync(fileC, Buffer.alloc(1024, 0)); + createFileBackedAttachment({ + filename: 'c.mp4', + mimeType: 'video/mp4', + sizeBytes: 1024, + filePath: fileC, + expiresAt: now + 86400000, + }); + + const result = runCleanupPass(); + + expect(result.cleaned).toBe(2); + expect(result.bytesFreed).toBe(2048 + 4096); + + // Expired files gone + expect(existsSync(fileA)).toBe(false); + expect(existsSync(fileB)).toBe(false); + + // Non-expired file still present + expect(existsSync(fileC)).toBe(true); + }); + + test('returns zeros when no expired attachments exist', () => { + const result = runCleanupPass(); + expect(result.cleaned).toBe(0); + expect(result.bytesFreed).toBe(0); + }); + + test('file-backed attachments without expiresAt are never cleaned', () => { + const recordingsDir = join(testDir, 'recordings-no-expiry'); + mkdirSync(recordingsDir, { recursive: true }); + + const filePath = join(recordingsDir, 'permanent.mp4'); + writeFileSync(filePath, Buffer.alloc(256, 0)); + + const permanent = createFileBackedAttachment({ + filename: 'permanent.mp4', + mimeType: 'video/mp4', + sizeBytes: 256, + filePath, + // No expiresAt — should never be cleaned + }); + + const result = runCleanupPass(); + + expect(result.cleaned).toBe(0); + expect(existsSync(filePath)).toBe(true); + expect(getAttachmentById(permanent.id)).not.toBeNull(); + }); +}); diff --git a/assistant/src/config/defaults.ts b/assistant/src/config/defaults.ts index f21bcf5bcff..4bd080e809f 100644 --- a/assistant/src/config/defaults.ts +++ b/assistant/src/config/defaults.ts @@ -252,6 +252,10 @@ export const DEFAULT_CONFIG: AssistantConfig = { allowPerCallOverride: true, }, }, + qaRecording: { + defaultRetentionDays: 7, + cleanupIntervalMs: 6 * 60 * 60 * 1000, // 6 hours + }, sms: { enabled: false, provider: 'twilio' as const, diff --git a/assistant/src/config/schema.ts b/assistant/src/config/schema.ts index ba2742b08cd..de98032bb45 100644 --- a/assistant/src/config/schema.ts +++ b/assistant/src/config/schema.ts @@ -1059,6 +1059,19 @@ export const SkillsConfigSchema = z.object({ allowBundled: z.array(z.string()).nullable().default(null), }); +export const QaRecordingConfigSchema = z.object({ + defaultRetentionDays: z + .number({ error: 'qaRecording.defaultRetentionDays must be a number' }) + .int('qaRecording.defaultRetentionDays must be an integer') + .positive('qaRecording.defaultRetentionDays must be a positive integer') + .default(7), + cleanupIntervalMs: z + .number({ error: 'qaRecording.cleanupIntervalMs must be a number' }) + .int('qaRecording.cleanupIntervalMs must be an integer') + .positive('qaRecording.cleanupIntervalMs must be a positive integer') + .default(6 * 60 * 60 * 1000), +}); + export const SmsConfigSchema = z.object({ enabled: z .boolean({ error: 'sms.enabled must be a boolean' }) @@ -1372,6 +1385,10 @@ export const AssistantConfigSchema = z.object({ allowPerCallOverride: true, }, }), + qaRecording: QaRecordingConfigSchema.default({ + defaultRetentionDays: 7, + cleanupIntervalMs: 6 * 60 * 60 * 1000, + }), sms: SmsConfigSchema.default({ enabled: false, provider: 'twilio', @@ -1441,5 +1458,6 @@ export type CallsSafetyConfig = z.infer; export type CallsVoiceConfig = z.infer; export type CallsElevenLabsConfig = z.infer; export type CallerIdentityConfig = z.infer; +export type QaRecordingConfig = z.infer; export type SmsConfig = z.infer; export type IngressConfig = z.infer; diff --git a/assistant/src/config/types.ts b/assistant/src/config/types.ts index 087de1c37c9..4cea1f98f92 100644 --- a/assistant/src/config/types.ts +++ b/assistant/src/config/types.ts @@ -37,6 +37,7 @@ export type { CallsVoiceConfig, CallsElevenLabsConfig, CallerIdentityConfig, + QaRecordingConfig, SmsConfig, IngressConfig, } from './schema.js'; diff --git a/assistant/src/daemon/lifecycle.ts b/assistant/src/daemon/lifecycle.ts index e7f5505c285..faad1f665d8 100644 --- a/assistant/src/daemon/lifecycle.ts +++ b/assistant/src/daemon/lifecycle.ts @@ -52,6 +52,7 @@ import { AgentHeartbeatService } from '../agent-heartbeat/agent-heartbeat-servic import { getEnrichmentService } from '../workspace/commit-message-enrichment-service.js'; import { reconcileCallsOnStartup } from '../calls/call-recovery.js'; import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js'; +import { startRecordingCleanup, stopRecordingCleanup } from './recording-cleanup.js'; const log = getLogger('lifecycle'); @@ -498,6 +499,9 @@ export async function runDaemon(): Promise { } } + // Start periodic cleanup of expired file-backed QA recording attachments. + startRecordingCleanup(config.qaRecording.cleanupIntervalMs); + // Start workspace heartbeat service. This periodically checks all // tracked workspaces for uncommitted changes and auto-commits when // thresholds are exceeded (age > 5 min OR > 20 files changed). @@ -572,6 +576,7 @@ export async function runDaemon(): Promise { if (runtimeHttp) await runtimeHttp.stop(); await browserManager.closeAllPages(); + stopRecordingCleanup(); scheduler.stop(); memoryWorker.stop(); await qdrantManager.stop(); diff --git a/assistant/src/daemon/recording-cleanup.ts b/assistant/src/daemon/recording-cleanup.ts new file mode 100644 index 00000000000..4b10323c674 --- /dev/null +++ b/assistant/src/daemon/recording-cleanup.ts @@ -0,0 +1,100 @@ +/** + * Periodic cleanup of expired file-backed QA recording attachments. + * + * Runs on a configurable interval (default: every 6 hours) and also + * executes a single pass on daemon startup to catch recordings that + * expired while the daemon was offline. + */ + +import { existsSync, statSync, unlinkSync } from 'node:fs'; +import { getExpiredFileAttachments, deleteFileBackedAttachment } from '../memory/attachments-store.js'; +import { getLogger } from '../util/logger.js'; + +const log = getLogger('recording-cleanup'); + +/** + * Run a single cleanup pass: find expired file-backed attachments, + * delete their files from disk, and remove the DB rows. + * + * Returns the number of cleaned-up attachments and total bytes freed. + */ +export function runCleanupPass(): { cleaned: number; bytesFreed: number } { + const expired = getExpiredFileAttachments(); + if (expired.length === 0) { + return { cleaned: 0, bytesFreed: 0 }; + } + + let cleaned = 0; + let bytesFreed = 0; + + for (const { id, filePath } of expired) { + try { + let fileSize = 0; + + if (existsSync(filePath)) { + try { + fileSize = statSync(filePath).size; + } catch { + // If we can't stat, still try to delete + } + unlinkSync(filePath); + log.info({ attachmentId: id, filePath }, 'Deleted expired recording file'); + } else { + log.debug({ attachmentId: id, filePath }, 'Expired recording file already missing from disk'); + } + + const result = deleteFileBackedAttachment(id); + if (result === 'deleted') { + cleaned++; + bytesFreed += fileSize; + } + } catch (err) { + log.warn({ err, attachmentId: id, filePath }, 'Failed to clean up expired recording'); + } + } + + if (cleaned > 0) { + const mbFreed = (bytesFreed / (1024 * 1024)).toFixed(1); + log.info({ count: cleaned, bytesFreed, mbFreed }, `Cleaned up ${cleaned} expired QA recordings, freed ${mbFreed} MB`); + } + + return { cleaned, bytesFreed }; +} + +let cleanupTimer: ReturnType | null = null; + +/** + * Start the periodic cleanup worker. Runs one immediate pass, + * then schedules recurring passes at the configured interval. + */ +export function startRecordingCleanup(intervalMs: number): void { + // Run one pass immediately to catch anything that expired while offline + try { + runCleanupPass(); + } catch (err) { + log.warn({ err }, 'Initial recording cleanup pass failed'); + } + + cleanupTimer = setInterval(() => { + try { + runCleanupPass(); + } catch (err) { + log.warn({ err }, 'Periodic recording cleanup pass failed'); + } + }, intervalMs); + + // Don't keep the process alive just for cleanup + cleanupTimer.unref(); + log.info({ intervalMs }, 'Recording cleanup worker started'); +} + +/** + * Stop the periodic cleanup worker. + */ +export function stopRecordingCleanup(): void { + if (cleanupTimer !== null) { + clearInterval(cleanupTimer); + cleanupTimer = null; + log.info('Recording cleanup worker stopped'); + } +} From 00041b8b2bc1532adab947f60f5d8e567263f5f0 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:29:09 -0500 Subject: [PATCH 09/72] fix: finalize QA recording before abort and use defer for recorder cleanup (#6920) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../ComputerUse/ScreenRecorder.swift | 67 ++++++++++++------- .../ComputerUse/Session.swift | 11 +-- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift index a2d380adaca..bd4c5de92e3 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift @@ -257,65 +257,80 @@ final class ScreenRecorder: NSObject, ScreenRecording { throw ScreenRecorderError.notRecording } + // Capture values needed for result computation before cleanup + let capturedStream = stream + let capturedWriter = writer + let capturedFileURL = fileURL + let capturedStartTime = recordingStartTime + let capturedVideoInput = videoInput + let capturedAudioInput = audioInput + let capturedWidth = captureWidth + let capturedHeight = captureHeight + let capturedScope = captureScope + let capturedIncludesAudio = includesAudio + let capturedTargetBundleId = targetBundleId + + // Guarantee state cleanup on all paths (including throws) + defer { + self.stream = nil + self.assetWriter = nil + self.videoInput = nil + self.audioInput = nil + self.outputHandler = nil + self.recordingFileURL = nil + self.recordingStartTime = nil + self.isRecording = false + } + // Stop the stream capture do { - try await stream.stopCapture() + try await capturedStream.stopCapture() } catch { log.warning("Error stopping stream capture: \(error.localizedDescription)") } // Mark inputs as finished - videoInput?.markAsFinished() - audioInput?.markAsFinished() + capturedVideoInput?.markAsFinished() + capturedAudioInput?.markAsFinished() // Finalize the asset writer - await writer.finishWriting() + await capturedWriter.finishWriting() - if writer.status == .failed { - let errorMsg = writer.error?.localizedDescription ?? "Unknown error" + if capturedWriter.status == .failed { + let errorMsg = capturedWriter.error?.localizedDescription ?? "Unknown error" log.error("Asset writer failed: \(errorMsg)") throw ScreenRecorderError.assetWriterFailed(errorMsg) } // Compute metadata - let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + let fileAttributes = try FileManager.default.attributesOfItem(atPath: capturedFileURL.path) let sizeBytes = (fileAttributes[.size] as? Int) ?? 0 // Compute duration from the asset - let asset = AVAsset(url: fileURL) + let asset = AVAsset(url: capturedFileURL) let duration: CMTime if let tracks = try? await asset.load(.tracks), !tracks.isEmpty { duration = try await asset.load(.duration) } else { // Fallback: estimate from wall clock time - let elapsed = recordingStartTime.map { Date().timeIntervalSince($0) } ?? 0 + let elapsed = capturedStartTime.map { Date().timeIntervalSince($0) } ?? 0 duration = CMTime(seconds: elapsed, preferredTimescale: 1000) } let durationMs = Int(CMTimeGetSeconds(duration) * 1000) let result = RecordingResult( - fileURL: fileURL, + fileURL: capturedFileURL, mimeType: "video/mp4", sizeBytes: sizeBytes, durationMs: durationMs, - width: captureWidth, - height: captureHeight, - captureScope: captureScope, - includeAudio: includesAudio, - targetBundleId: targetBundleId + width: capturedWidth, + height: capturedHeight, + captureScope: capturedScope, + includeAudio: capturedIncludesAudio, + targetBundleId: capturedTargetBundleId ) - // Clean up state - self.stream = nil - self.assetWriter = nil - self.videoInput = nil - self.audioInput = nil - self.outputHandler = nil - self.recordingFileURL = nil - self.recordingStartTime = nil - self.isRecording = false - - log.info("Screen recording stopped: \(fileURL.lastPathComponent) (\(sizeBytes) bytes, \(durationMs)ms)") + log.info("Screen recording stopped: \(capturedFileURL.lastPathComponent) (\(sizeBytes) bytes, \(durationMs)ms)") return result } diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index f76cbb971c3..990f31d03fd 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -181,15 +181,18 @@ final class ComputerUseSession: ObservableObject { } } else { state = .failed(reason: "No focused window and screen capture failed") + logger.finishSession(result: "failed: no window") + // Finalize QA recording BEFORE sending abort — the daemon's handleCuSessionAbort + // deletes cuSessionMetadata, so cu_session_finalized must arrive first for + // summary injection to work. + if qaMode { + await finalizeQARecording() + } do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) } catch { log.error("Failed to send session abort message: \(error)") } - logger.finishSession(result: "failed: no window") - if qaMode { - await finalizeQARecording() - } return } From 57595f55b5324cb06e8e0933c857cb0efaa310d5 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:39:47 -0500 Subject: [PATCH 10/72] fix: clean up cuSessionMetadata on socket disconnect and add NOT NULL to storage_kind (#6924) Co-authored-by: Vellum Assistant --- assistant/src/daemon/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/assistant/src/daemon/server.ts b/assistant/src/daemon/server.ts index 5cdc4cb687c..3d836781bac 100644 --- a/assistant/src/daemon/server.ts +++ b/assistant/src/daemon/server.ts @@ -434,6 +434,7 @@ export class DaemonServer { if (cuSessionIds) { for (const cuSessionId of cuSessionIds) { this.cuObservationParseSequence.delete(cuSessionId); + this.cuSessionMetadata.delete(cuSessionId); const cuSession = this.cuSessions.get(cuSessionId); if (cuSession) { cuSession.abort(); From 2a89a60575b9a198957353449c8e9c19a92b6267 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:41:20 -0500 Subject: [PATCH 11/72] fix: clamp cleanup interval to setInterval-safe maximum (#6925) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/config/schema.ts | 1 + assistant/src/daemon/recording-cleanup.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/assistant/src/config/schema.ts b/assistant/src/config/schema.ts index de98032bb45..f1d2ba486b1 100644 --- a/assistant/src/config/schema.ts +++ b/assistant/src/config/schema.ts @@ -1069,6 +1069,7 @@ export const QaRecordingConfigSchema = z.object({ .number({ error: 'qaRecording.cleanupIntervalMs must be a number' }) .int('qaRecording.cleanupIntervalMs must be an integer') .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), }); diff --git a/assistant/src/daemon/recording-cleanup.ts b/assistant/src/daemon/recording-cleanup.ts index 4b10323c674..5e8d0050528 100644 --- a/assistant/src/daemon/recording-cleanup.ts +++ b/assistant/src/daemon/recording-cleanup.ts @@ -75,13 +75,18 @@ export function startRecordingCleanup(intervalMs: number): void { log.warn({ err }, 'Initial recording cleanup pass failed'); } + // setInterval uses a 32-bit signed int internally; values above 2^31-1 ms + // (~24.8 days) wrap around and fire near-continuously. + const MAX_INTERVAL_MS = 2_147_483_647; + const safeInterval = Math.min(intervalMs, MAX_INTERVAL_MS); + cleanupTimer = setInterval(() => { try { runCleanupPass(); } catch (err) { log.warn({ err }, 'Periodic recording cleanup pass failed'); } - }, intervalMs); + }, safeInterval); // Don't keep the process alive just for cleanup cleanupTimer.unref(); From ba6b6b9bb7c1b4e9ea71ef80627af43bf53bcacb Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:41:22 -0500 Subject: [PATCH 12/72] fix: defer abort message in cancel() for QA mode until after finalization (#6926) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../ComputerUse/Session.swift | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 990f31d03fd..001c4be6684 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -279,6 +279,17 @@ final class ComputerUseSession: ObservableObject { // Finalize QA recording and send cu_session_finalized if qaMode { await finalizeQARecording() + + // Now that finalization is complete, send the deferred abort for cancelled + // QA sessions. cancel() skips sending it so cu_session_finalized arrives + // before the abort (the daemon's handleCuSessionAbort deletes metadata). + if isCancelled { + do { + try daemonClient.send(CuSessionAbortMessage(sessionId: id)) + } catch { + log.error("Failed to send deferred session abort after QA finalization: \(error)") + } + } } } @@ -1034,11 +1045,15 @@ final class ComputerUseSession: ObservableObject { messageLoopTask?.cancel() confirmationContinuation?.resume(returning: false) confirmationContinuation = nil - // Tell the daemon to abort the server-side CU session so it stops burning tokens - do { - try daemonClient.send(CuSessionAbortMessage(sessionId: id)) - } catch { - log.error("Failed to send session abort on cancel: \(error)") + // In QA mode, defer the abort until after finalizeQARecording() in run(), + // because the daemon's handleCuSessionAbort deletes cuSessionMetadata and + // cu_session_finalized must arrive first for summary injection to work. + if !qaMode { + do { + try daemonClient.send(CuSessionAbortMessage(sessionId: id)) + } catch { + log.error("Failed to send session abort on cancel: \(error)") + } } } From 7b3874eae0ae8f924060818b22be5676872226c2 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:45:43 -0500 Subject: [PATCH 13/72] fix: keep session in socketToCuSession until socket close to prevent metadata leak (#6931) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/handlers/computer-use.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index a0d648f49f7..885cd78439e 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -33,11 +33,13 @@ function removeCuSessionReferences( // end of handleCuSessionFinalized instead. cuObservationSequenceBySession.delete(sessionId); ctx.cuObservationParseSequence.delete(sessionId); - for (const [sock, ids] of ctx.socketToCuSession) { - if (ids.delete(sessionId) && ids.size === 0) { - ctx.socketToCuSession.delete(sock); - } - } + // NOTE: socketToCuSession is intentionally NOT cleaned up here. + // The socket close handler in server.ts is the sole owner of + // socketToCuSession cleanup — it uses the mapping to find and delete + // cuSessionMetadata entries. Removing the session here would race: + // if a CU session reaches terminal state (triggering this function) + // and then the socket disconnects before cu_session_finalized, + // the close handler wouldn't see the session and metadata would leak. } export function handleCuSessionCreate( From 2cc5a0b808d8ad733ac7d30fdf7fa453abce6413 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:47:36 -0500 Subject: [PATCH 14/72] fix: add safety-net abort in cancel() for QA mode with delay (#6932) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../ComputerUse/Session.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 001c4be6684..3c2b54d143b 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -280,9 +280,9 @@ final class ComputerUseSession: ObservableObject { if qaMode { await finalizeQARecording() - // Now that finalization is complete, send the deferred abort for cancelled - // QA sessions. cancel() skips sending it so cu_session_finalized arrives - // before the abort (the daemon's handleCuSessionAbort deletes metadata). + // Send the abort immediately after finalization for cancelled QA sessions. + // This arrives before cancel()'s 2-second safety-net abort fires. + // The daemon deduplicates aborts, so the safety net is harmless if we get here. if isCancelled { do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) @@ -1045,10 +1045,17 @@ final class ComputerUseSession: ObservableObject { messageLoopTask?.cancel() confirmationContinuation?.resume(returning: false) confirmationContinuation = nil - // In QA mode, defer the abort until after finalizeQARecording() in run(), - // because the daemon's handleCuSessionAbort deletes cuSessionMetadata and - // cu_session_finalized must arrive first for summary injection to work. - if !qaMode { + + if qaMode { + // Deferred abort: give run() a chance to send finalization first, + // but guarantee abort eventually fires as a safety net in case + // run() never reaches the post-loop block (e.g., throws or gets stuck). + Task { @MainActor in + try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + guard self.isCancelled else { return } // in case state changed + try? self.daemonClient.send(CuSessionAbortMessage(sessionId: self.id)) + } + } else { do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) } catch { From 4d57a54b1e3038c74d93cfdc2be61f3576fa036c Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:50:14 -0500 Subject: [PATCH 15/72] fix: remove stale socketToCuSession entry on CU session replacement (#6933) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/handlers/computer-use.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 885cd78439e..de877fdd480 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -57,6 +57,13 @@ export function handleCuSessionCreate( // Clean up stale metadata from the replaced session; the new session // will set its own metadata below if needed. ctx.cuSessionMetadata.delete(msg.sessionId); + // Remove the session ID from the old socket's set so the old socket's + // close handler won't abort the replacement session. + for (const [sock, ids] of ctx.socketToCuSession) { + if (ids.delete(msg.sessionId) && ids.size === 0) { + ctx.socketToCuSession.delete(sock); + } + } } const config = getConfig(); From fd679672b7ed7872286cbb4d3266e60926104ffd Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 11:51:45 -0500 Subject: [PATCH 16/72] fix: cancel safety-net timer when run() reaches finalization (#6934) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../vellum-assistant/ComputerUse/Session.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 3c2b54d143b..72e80d2da07 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -48,6 +48,7 @@ final class ComputerUseSession: ObservableObject { private var isPaused = false private var confirmationContinuation: CheckedContinuation? private var messageLoopTask: Task? + private var cancelSafetyNetTask: Task? private let enumerator: AccessibilityTreeProviding private let screenCapture: ScreenCaptureProviding @@ -182,6 +183,11 @@ final class ComputerUseSession: ObservableObject { } else { state = .failed(reason: "No focused window and screen capture failed") logger.finishSession(result: "failed: no window") + // Disarm the cancel safety net — run() reached the post-loop and will + // handle finalization + abort itself. + cancelSafetyNetTask?.cancel() + cancelSafetyNetTask = nil + // Finalize QA recording BEFORE sending abort — the daemon's handleCuSessionAbort // deletes cuSessionMetadata, so cu_session_finalized must arrive first for // summary injection to work. @@ -276,6 +282,11 @@ final class ComputerUseSession: ObservableObject { } } + // Disarm the cancel safety net — run() reached the post-loop and will + // handle finalization + abort itself. + cancelSafetyNetTask?.cancel() + cancelSafetyNetTask = nil + // Finalize QA recording and send cu_session_finalized if qaMode { await finalizeQARecording() @@ -1050,7 +1061,7 @@ final class ComputerUseSession: ObservableObject { // Deferred abort: give run() a chance to send finalization first, // but guarantee abort eventually fires as a safety net in case // run() never reaches the post-loop block (e.g., throws or gets stuck). - Task { @MainActor in + cancelSafetyNetTask = Task { @MainActor in try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds guard self.isCancelled else { return } // in case state changed try? self.daemonClient.send(CuSessionAbortMessage(sessionId: self.id)) From c648898745310fd0166afad094e05a1bed8ded03 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:09:31 -0500 Subject: [PATCH 17/72] feat: QA intent detection + routing metadata + epoch fix (#6941) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/handlers/misc.ts | 4 ++++ assistant/src/daemon/handlers/shared.ts | 6 ++++++ assistant/src/daemon/ipc-contract.ts | 4 ++++ assistant/src/daemon/qa-intent.ts | 21 +++++++++++++++++++ .../ComputerUse/Session.swift | 6 +++--- .../IPC/Generated/IPCContractGenerated.swift | 12 ++++------- 6 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 assistant/src/daemon/qa-intent.ts diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index a086c80c17e..7dedba45b4c 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -20,6 +20,7 @@ import type { } from '../ipc-protocol.js'; import { log, wireEscalationHandler, renderHistoryContent, defineHandlers, type HandlerContext } from './shared.js'; import { handleCuSessionCreate } from './computer-use.js'; +import { detectQaIntent } from '../qa-intent.js'; // ─── Task submit handler ──────────────────────────────────────────────────── @@ -74,6 +75,7 @@ export async function handleTaskSubmit( if (interactionType === 'computer_use') { // Create CU session (reuse handleCuSessionCreate logic) const sessionId = uuid(); + const isQa = detectQaIntent(msg.task); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId, @@ -82,6 +84,7 @@ export async function handleTaskSubmit( screenHeight: msg.screenHeight, attachments: msg.attachments, interactionType: 'computer_use', + ...(isQa ? { qaMode: true } : {}), }; handleCuSessionCreate(cuMsg, socket, ctx); @@ -89,6 +92,7 @@ export async function handleTaskSubmit( type: 'task_routed', sessionId, interactionType: 'computer_use', + ...(isQa ? { qaMode: true } : {}), }); } else { // Create text QA session and immediately start processing diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index ef084d730c9..48808692f1c 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -9,6 +9,7 @@ import type { ClientMessage, CuSessionCreate, ServerMessage, SessionTransportMet import type { SecretPromptResult } from '../../permissions/secret-prompter.js'; import { getConfig } from '../../config/loader.js'; import type { DebouncerMap } from '../../util/debounce.js'; +import { detectQaIntent } from '../qa-intent.js'; const log = getLogger('handlers'); @@ -235,6 +236,7 @@ export function wireEscalationHandler( } const cuSessionId = uuid(); + const isQa = detectQaIntent(task); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId: cuSessionId, @@ -242,6 +244,8 @@ export function wireEscalationHandler( screenWidth, screenHeight, interactionType: 'computer_use', + reportToSessionId: sourceSessionId, + ...(isQa ? { qaMode: true } : {}), }; handleCuSessionCreate(cuMsg, currentSocket, ctx); @@ -251,6 +255,8 @@ export function wireEscalationHandler( interactionType: 'computer_use', task, escalatedFrom: sourceSessionId, + reportToSessionId: sourceSessionId, + ...(isQa ? { qaMode: true } : {}), }); return true; diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 2aafcf5bcdc..f213c6b03fd 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -1566,6 +1566,10 @@ export interface TaskRouted { task?: string; /** Set when a text_qa session escalates to computer_use via computer_use_request_control. */ escalatedFrom?: string; + /** Whether this is a QA/test workflow session. */ + qaMode?: boolean; + /** The originating chat session ID for result injection. */ + reportToSessionId?: string; } export interface RideShotgunProgress { diff --git a/assistant/src/daemon/qa-intent.ts b/assistant/src/daemon/qa-intent.ts new file mode 100644 index 00000000000..45114c8f6ed --- /dev/null +++ b/assistant/src/daemon/qa-intent.ts @@ -0,0 +1,21 @@ +/** + * Detect whether a user's task text indicates a QA/test workflow. + * Uses keyword/pattern matching for v1 — can be upgraded to semantic detection later. + */ +export function detectQaIntent(taskText: string): boolean { + const lower = taskText.toLowerCase().trim(); + + // Direct QA/test commands + if (/^(qa|test|verify|check)\b/.test(lower)) return true; + + // Natural language QA patterns + const qaPatterns = [ + /\b(run|do|perform|execute)\s+(a\s+)?(qa|test|check|verification)\b/, + /\b(test|qa|verify|check)\s+(this|the|that|my)\b/, + /\bhelp\s+me\s+(test|qa|verify|check)\b/, + /\b(can you|could you|please)\s+(test|qa|verify|check)\b/, + /\btesting\s+(the|this|that|my)\b/, + ]; + + return qaPatterns.some(p => p.test(lower)); +} diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 72e80d2da07..b626805806b 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -995,8 +995,8 @@ final class ComputerUseSession: ObservableObject { summary = "Session cancelled by user" stepCount = currentStepNumber default: - status = "unknown" - summary = "Session ended in unexpected state" + status = "failed" + summary = "Session ended in unexpected state: \(state)" stepCount = currentStepNumber } @@ -1005,7 +1005,7 @@ final class ComputerUseSession: ObservableObject { if let recorder = screenRecorder, recorder.isRecording { do { let result = try await recorder.stopRecording() - let expiresAtEpoch = Int(Date().addingTimeInterval(7 * 24 * 3600).timeIntervalSince1970) + let expiresAtEpoch = Int(Date().addingTimeInterval(7 * 24 * 3600).timeIntervalSince1970 * 1000) recordingData = IPCCuSessionFinalizedRecording( localPath: result.fileURL.path, mimeType: result.mimeType, diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index ff679d86690..649df0c26d4 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -3440,14 +3440,10 @@ public struct IPCTaskRouted: Codable, Sendable { public let task: String? /// Set when a text_qa session escalates to computer_use via computer_use_request_control. public let escalatedFrom: String? - - public init(type: String, sessionId: String, interactionType: String, task: String?, escalatedFrom: String?) { - self.type = type - self.sessionId = sessionId - self.interactionType = interactionType - self.task = task - self.escalatedFrom = escalatedFrom - } + /// Whether this is a QA/test workflow session. + public let qaMode: Bool? + /// The originating chat session ID for result injection. + public let reportToSessionId: String? } /// Server push — broadcast when a task run creates a conversation, so the client can show it as a chat thread. From 93a3ee40052212b1d83262c15bb85d95d93d952c Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:11:34 -0500 Subject: [PATCH 18/72] feat: create file-backed attachment on finalize + video view /content support (#6943) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/handlers/computer-use.ts | 65 +++++++++++++----- assistant/src/daemon/handlers/sessions.ts | 6 +- .../InlineVideoAttachmentView.swift | 67 ++++++++++++------- 3 files changed, 93 insertions(+), 45 deletions(-) diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index de877fdd480..1be40acd6f9 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -12,6 +12,7 @@ import type { ServerMessage, } from '../ipc-protocol.js'; import * as conversationStore from '../../memory/conversation-store.js'; +import { createFileBackedAttachment, linkAttachmentToMessage } from '../../memory/attachments-store.js'; import { log, defineHandlers, findSocketForSession, type HandlerContext, type CuSessionMetadata } from './shared.js'; const cuObservationSequenceBySession = new Map(); @@ -231,23 +232,6 @@ export function handleCuSessionFinalized( 'CU session finalized by client', ); - // If recording metadata is present, log it for future M3 file-backed attachment support. - if (msg.recording) { - log.info( - { - sessionId: msg.sessionId, - localPath: msg.recording.localPath, - mimeType: msg.recording.mimeType, - sizeBytes: msg.recording.sizeBytes, - durationMs: msg.recording.durationMs, - width: msg.recording.width, - height: msg.recording.height, - captureScope: msg.recording.captureScope, - }, - 'CU session recording metadata (stored for M3)', - ); - } - // Inject a summary message into the originating chat session if configured. if (meta?.reportToSessionId && msg.summary) { const reportSessionId = meta.reportToSessionId; @@ -258,7 +242,7 @@ export function handleCuSessionFinalized( const conversation = conversationStore.getConversation(reportSessionId); if (conversation) { const assistantContent = JSON.stringify([{ type: 'text', text: msg.summary }]); - conversationStore.addMessage(reportSessionId, 'assistant', assistantContent, { + const persistedMessage = conversationStore.addMessage(reportSessionId, 'assistant', assistantContent, { source: 'cu_session_finalized', cuSessionId: msg.sessionId, cuStatus: msg.status, @@ -267,6 +251,42 @@ export function handleCuSessionFinalized( ...(msg.recording ? { recordingPath: msg.recording.localPath } : {}), }); + // Create a file-backed attachment from the recording and link it to the message. + let recordingAttachment: { id: string; filename: string; mimeType: string; sizeBytes: number } | undefined; + if (msg.recording) { + try { + const attachment = createFileBackedAttachment({ + filename: `qa-recording-${msg.sessionId}.mp4`, + mimeType: msg.recording.mimeType || 'video/mp4', + sizeBytes: msg.recording.sizeBytes, + filePath: msg.recording.localPath, + sha256: undefined, + expiresAt: msg.recording.expiresAt, + }); + linkAttachmentToMessage(persistedMessage.id, attachment.id, 0); + recordingAttachment = { + id: attachment.id, + filename: attachment.originalFilename, + mimeType: attachment.mimeType, + sizeBytes: attachment.sizeBytes, + }; + log.info( + { + sessionId: msg.sessionId, + attachmentId: attachment.id, + filePath: msg.recording.localPath, + sizeBytes: msg.recording.sizeBytes, + }, + 'Created file-backed attachment for CU session recording', + ); + } catch (err) { + log.error( + { err, sessionId: msg.sessionId, localPath: msg.recording.localPath }, + 'Failed to create file-backed attachment for recording', + ); + } + } + // Also append to the in-memory Session.messages so subsequent turns // in the same session see the injected summary without a reload. const activeSession = ctx.sessions.get(reportSessionId); @@ -288,6 +308,15 @@ export function handleCuSessionFinalized( ctx.send(reportSocket, { type: 'message_complete', sessionId: reportSessionId, + ...(recordingAttachment ? { + attachments: [{ + id: recordingAttachment.id, + filename: recordingAttachment.filename, + mimeType: recordingAttachment.mimeType, + data: '', + sizeBytes: recordingAttachment.sizeBytes, + }], + } : {}), }); } diff --git a/assistant/src/daemon/handlers/sessions.ts b/assistant/src/daemon/handlers/sessions.ts index e5b47fc3f63..30530ea38ba 100644 --- a/assistant/src/daemon/handlers/sessions.ts +++ b/assistant/src/daemon/handlers/sessions.ts @@ -395,10 +395,12 @@ export function handleHistoryRequest( // the client, so non-video attachments always keep their inline data. const MAX_INLINE_B64_SIZE = 512 * 1024; attachments = linked.map((a) => { - const omit = a.mimeType.startsWith('video/') && a.dataBase64.length > MAX_INLINE_B64_SIZE; + const isFileBacked = a.storageKind === 'file'; + const omit = isFileBacked || (a.mimeType.startsWith('video/') && a.dataBase64.length > MAX_INLINE_B64_SIZE); // Lazily generate thumbnails for existing video attachments on first history load. - if (a.mimeType.startsWith('video/') && !a.thumbnailBase64) { + // Skip for file-backed attachments since they have no inline base64 to extract from. + if (a.mimeType.startsWith('video/') && !a.thumbnailBase64 && !isFileBacked) { const attachmentId = a.id; const base64 = a.dataBase64; generateVideoThumbnail(base64).then((thumb) => { diff --git a/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift b/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift index 5a514888175..c414654587a 100644 --- a/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/MediaEmbeds/InlineVideoAttachmentView.swift @@ -173,8 +173,15 @@ struct InlineVideoAttachmentView: View { // Server thumbnail and aspect ratio are set eagerly in init. if thumbnailImage != nil { return } - // Fallback: extract thumbnail from inline video data. - guard !attachment.data.isEmpty, let data = Data(base64Encoded: attachment.data) else { return } + // Try inline base64 first, then fall back to /content for lazy-loaded attachments. + var videoData: Data? + if !attachment.data.isEmpty { + videoData = Data(base64Encoded: attachment.data) + } else if attachment.isLazyLoad, let port = daemonHttpPort, !attachment.id.isEmpty { + videoData = try? await fetchAttachmentContent(port: port, attachmentId: attachment.id) + } + + guard let data = videoData else { return } let fileURL = safeTempURL() do { @@ -272,9 +279,11 @@ struct InlineVideoAttachmentView: View { isLoading = true Task { do { - let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId) + let data = try await fetchAttachmentContent(port: port, attachmentId: attachmentId) + let fileURL = safeTempURL() + try data.write(to: fileURL) await MainActor.run { isLoading = false } - await playFromBase64(base64) + await playFromFile(fileURL) } catch { log.error("Failed to fetch attachment \(attachmentId): \(error.localizedDescription)") await MainActor.run { @@ -318,11 +327,7 @@ struct InlineVideoAttachmentView: View { } Task { do { - let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId) - guard let data = Data(base64Encoded: base64) else { - await MainActor.run { isSaving = false } - return - } + let data = try await fetchAttachmentContent(port: port, attachmentId: attachmentId) try data.write(to: destURL) await MainActor.run { isSaving = false } } catch { @@ -373,11 +378,7 @@ struct InlineVideoAttachmentView: View { ) { completion in Task { do { - let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId) - guard let data = Data(base64Encoded: base64) else { - completion(nil, false, URLError(.cannotDecodeContentData)) - return - } + let data = try await fetchAttachmentContent(port: port, attachmentId: attachmentId) try data.write(to: fileURL) completion(fileURL, true, nil) } catch { @@ -400,14 +401,7 @@ struct InlineVideoAttachmentView: View { isLoading = true Task { do { - let base64 = try await fetchAttachmentData(port: port, attachmentId: attachmentId) - guard let data = Data(base64Encoded: base64) else { - await MainActor.run { - isLoading = false - failed = true - } - return - } + let data = try await fetchAttachmentContent(port: port, attachmentId: attachmentId) let fileURL = safeTempURL() try data.write(to: fileURL) await MainActor.run { @@ -415,7 +409,10 @@ struct InlineVideoAttachmentView: View { NSWorkspace.shared.open(fileURL) } } catch { - await MainActor.run { isLoading = false } + await MainActor.run { + isLoading = false + failed = true + } } } } else { @@ -427,8 +424,8 @@ struct InlineVideoAttachmentView: View { } } -/// Fetch attachment base64 data from the daemon HTTP endpoint. -private func fetchAttachmentData(port: Int, attachmentId: String) async throws -> String { +/// Read the daemon HTTP auth token from disk. +private func readDaemonToken() throws -> String { let tokenBase: String if let baseDir = ProcessInfo.processInfo.environment["BASE_DATA_DIR"]?.trimmingCharacters(in: .whitespacesAndNewlines), !baseDir.isEmpty { @@ -442,6 +439,12 @@ private func fetchAttachmentData(port: Int, attachmentId: String) async throws - !token.isEmpty else { throw URLError(.userAuthenticationRequired) } + return token +} + +/// Fetch attachment base64 data from the daemon HTTP endpoint. +private func fetchAttachmentData(port: Int, attachmentId: String) async throws -> String { + let token = try readDaemonToken() let url = URL(string: "http://localhost:\(port)/v1/attachments/\(attachmentId)")! var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") @@ -458,6 +461,20 @@ private func fetchAttachmentData(port: Int, attachmentId: String) async throws - return decoded.data } +/// Fetch attachment binary content from the daemon /content endpoint. +/// Returns raw bytes suitable for writing to disk or AVPlayer consumption. +private func fetchAttachmentContent(port: Int, attachmentId: String) async throws -> Data { + let token = try readDaemonToken() + var request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)/v1/attachments/\(attachmentId)/content")!) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + throw URLError(.badServerResponse) + } + return data +} + /// NSViewRepresentable wrapper for AVPlayerView. private struct VideoPlayerView: NSViewRepresentable { let player: AVPlayer From 72ee8bbc777bc43bc76e202635ecf9d6a38ab442 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:13:02 -0500 Subject: [PATCH 19/72] feat: wire QA mode from task_routed to ComputerUseSession (#6945) Co-authored-by: Vellum Assistant --- .../vellum-assistant/App/AppDelegate+Sessions.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 9a760db6414..7d8fdfdaa4d 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -53,7 +53,10 @@ extension AppDelegate { maxSteps: maxSteps, sessionId: routed.sessionId, skipSessionCreate: true, - notificationService: self.services.activityNotificationService + notificationService: self.services.activityNotificationService, + screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, + reportToSessionId: routed.reportToSessionId, + qaMode: routed.qaMode ?? false ) // Don't bind relatedViewModel for escalated sessions — the active view model // may be unrelated if the user switched threads. Tool calls for escalated @@ -193,7 +196,10 @@ extension AppDelegate { attachments: submission.attachments, sessionId: routed.sessionId, skipSessionCreate: true, - notificationService: self.services.activityNotificationService + notificationService: self.services.activityNotificationService, + screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, + reportToSessionId: routed.reportToSessionId, + qaMode: routed.qaMode ?? false ) // Don't bind relatedViewModel — sessions started via startSession() don't // originate from a chat thread, so there's no ChatViewModel to extract From 6a2049c703bb7b89981f75510fbc0e0c5629a67e Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:27:39 -0500 Subject: [PATCH 20/72] feat: wire reportToSessionId for direct QA + configurable retention days (#6951) P1 #3: Add conversationId to TaskSubmit so direct CU QA sessions can set reportToSessionId. Client passes active thread ID. Daemon routes it to CU session metadata, enabling attachment creation on finalize. P1 #4: Add retentionDays to TaskRouted from daemon config. Client reads it instead of hardcoding 7 days. Also: create file-backed attachment for recordings without reportToSessionId so cleanup can track orphan files. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/handlers/computer-use.ts | 18 ++++++++++++++++++ assistant/src/daemon/handlers/misc.ts | 9 +++++++-- assistant/src/daemon/handlers/shared.ts | 6 +++++- assistant/src/daemon/ipc-contract.ts | 4 ++++ .../App/AppDelegate+Sessions.swift | 12 +++++++++--- .../vellum-assistant/ComputerUse/Session.swift | 8 ++++++-- .../IPC/Generated/IPCContractGenerated.swift | 13 ++++--------- clients/shared/IPC/IPCMessages.swift | 4 ++-- 8 files changed, 55 insertions(+), 19 deletions(-) diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 1be40acd6f9..43e0694e50d 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -332,6 +332,24 @@ export function handleCuSessionFinalized( } } + // Create a file-backed attachment for recordings without a reporting session + // so cleanup can track orphan files. + if (msg.recording && !(meta?.reportToSessionId)) { + try { + createFileBackedAttachment({ + filename: `qa-recording-${msg.sessionId}.mp4`, + mimeType: msg.recording.mimeType || 'video/mp4', + sizeBytes: msg.recording.sizeBytes, + filePath: msg.recording.localPath, + sha256: undefined, + expiresAt: msg.recording.expiresAt, + }); + log.info({ sessionId: msg.sessionId }, 'Created orphan file-backed attachment for cleanup tracking (no reportToSessionId)'); + } catch (err) { + log.error({ err, sessionId: msg.sessionId }, 'Failed to create file-backed attachment for orphan recording'); + } + } + // Clean up all CU session state. removeCuSessionReferences(ctx, msg.sessionId); // Delete metadata last — after it has been consumed for summary injection diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index 7dedba45b4c..f987bb2f984 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -76,6 +76,7 @@ export async function handleTaskSubmit( // Create CU session (reuse handleCuSessionCreate logic) const sessionId = uuid(); const isQa = detectQaIntent(msg.task); + const config = getConfig(); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId, @@ -84,7 +85,7 @@ export async function handleTaskSubmit( screenHeight: msg.screenHeight, attachments: msg.attachments, interactionType: 'computer_use', - ...(isQa ? { qaMode: true } : {}), + ...(isQa ? { qaMode: true, reportToSessionId: msg.conversationId } : {}), }; handleCuSessionCreate(cuMsg, socket, ctx); @@ -92,7 +93,11 @@ export async function handleTaskSubmit( type: 'task_routed', sessionId, interactionType: 'computer_use', - ...(isQa ? { qaMode: true } : {}), + ...(isQa ? { + qaMode: true, + reportToSessionId: msg.conversationId, + retentionDays: config.qaRecording.defaultRetentionDays, + } : {}), }); } else { // Create text QA session and immediately start processing diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index 48808692f1c..044664f741d 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -237,6 +237,7 @@ export function wireEscalationHandler( const cuSessionId = uuid(); const isQa = detectQaIntent(task); + const config = getConfig(); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId: cuSessionId, @@ -256,7 +257,10 @@ export function wireEscalationHandler( task, escalatedFrom: sourceSessionId, reportToSessionId: sourceSessionId, - ...(isQa ? { qaMode: true } : {}), + ...(isQa ? { + qaMode: true, + retentionDays: config.qaRecording.defaultRetentionDays, + } : {}), }); return true; diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index f213c6b03fd..21e85130d1a 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -240,6 +240,8 @@ export interface TaskSubmit { screenHeight: number; attachments?: UserMessageAttachment[]; source?: 'voice' | 'text'; + /** The originating conversation/thread ID, if submitting from a chat context. */ + conversationId?: string; } export interface RideShotgunStart { @@ -1570,6 +1572,8 @@ export interface TaskRouted { qaMode?: boolean; /** The originating chat session ID for result injection. */ reportToSessionId?: string; + /** Recording retention in days (from daemon config). */ + retentionDays?: number; } export interface RideShotgunProgress { diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 7d8fdfdaa4d..22b3e99f268 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -56,7 +56,8 @@ extension AppDelegate { notificationService: self.services.activityNotificationService, screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, reportToSessionId: routed.reportToSessionId, - qaMode: routed.qaMode ?? false + qaMode: routed.qaMode ?? false, + retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7 ) // Don't bind relatedViewModel for escalated sessions — the active view model // may be unrelated if the user switched threads. Tool calls for escalated @@ -139,13 +140,17 @@ extension AppDelegate { extractedText: $0.extractedText ) } + // Pass the active thread's conversation ID so the daemon can set reportToSessionId for QA sessions + let activeConversationId = self.mainWindow?.threadManager.activeViewModel?.sessionId + do { try self.daemonClient.send(TaskSubmitMessage( task: effectiveTask, screenWidth: Int(screenBounds.width), screenHeight: Int(screenBounds.height), attachments: ipcAttachments, - source: submission.source + source: submission.source, + conversationId: activeConversationId )) } catch { log.error("Failed to send task submit message: \(error)") @@ -199,7 +204,8 @@ extension AppDelegate { notificationService: self.services.activityNotificationService, screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, reportToSessionId: routed.reportToSessionId, - qaMode: routed.qaMode ?? false + qaMode: routed.qaMode ?? false, + retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7 ) // 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 b626805806b..22e2fda09d7 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -40,6 +40,8 @@ final class ComputerUseSession: ObservableObject { let reportToSessionId: String? /// Whether this session is running in QA/test mode. let qaMode: Bool + /// Recording retention in days (from daemon config, default 7). + let retentionDays: Int /// Weak reference to the chat view model for extracting tool calls for notifications. weak var relatedViewModel: ChatViewModel? @@ -85,7 +87,8 @@ final class ComputerUseSession: ObservableObject { notificationService: ActivityNotificationServiceProtocol? = nil, screenRecorder: ScreenRecording? = nil, reportToSessionId: String? = nil, - qaMode: Bool = false + qaMode: Bool = false, + retentionDays: Int = 7 ) { self.id = sessionId ?? UUID().uuidString self.task = task @@ -103,6 +106,7 @@ final class ComputerUseSession: ObservableObject { self.screenRecorder = screenRecorder self.reportToSessionId = reportToSessionId self.qaMode = qaMode + self.retentionDays = retentionDays self.verifier = ActionVerifier(maxSteps: maxSteps) self.logger = SessionLogger(task: task, attachments: attachments) } @@ -1005,7 +1009,7 @@ final class ComputerUseSession: ObservableObject { if let recorder = screenRecorder, recorder.isRecording { do { let result = try await recorder.stopRecording() - let expiresAtEpoch = Int(Date().addingTimeInterval(7 * 24 * 3600).timeIntervalSince1970 * 1000) + let expiresAtEpoch = Int(Date().addingTimeInterval(Double(retentionDays) * 24 * 3600).timeIntervalSince1970 * 1000) recordingData = IPCCuSessionFinalizedRecording( localPath: result.fileURL.path, mimeType: result.mimeType, diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 649df0c26d4..8c49793940a 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -3444,6 +3444,8 @@ public struct IPCTaskRouted: Codable, Sendable { public let qaMode: Bool? /// The originating chat session ID for result injection. public let reportToSessionId: String? + /// Recording retention in days (from daemon config). + public let retentionDays: Double? } /// Server push — broadcast when a task run creates a conversation, so the client can show it as a chat thread. @@ -3477,15 +3479,8 @@ public struct IPCTaskSubmit: Codable, Sendable { public let screenHeight: Int public let attachments: [IPCUserMessageAttachment]? public let source: String? - - public init(type: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]?, source: String?) { - self.type = type - self.task = task - self.screenWidth = screenWidth - self.screenHeight = screenHeight - self.attachments = attachments - self.source = source - } + /// The originating conversation/thread ID, if submitting from a chat context. + public let conversationId: String? } public struct IPCTelegramConfigRequest: Codable, Sendable { diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index c960a7a0f00..dc70873c2b7 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -317,8 +317,8 @@ extension IPCUserMessage { public typealias TaskSubmitMessage = IPCTaskSubmit extension IPCTaskSubmit { - public init(task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, source: String?) { - self.init(type: "task_submit", task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, source: source) + public init(task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, source: String?, conversationId: String? = nil) { + self.init(type: "task_submit", task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, source: source, conversationId: conversationId) } } From 0db6e036f70217254c1036f859bf8256f149b5da Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:28:36 -0500 Subject: [PATCH 21/72] docs: fix ARCHITECTURE.md QA recording data flow accuracy (#6952) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- ARCHITECTURE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 821a6a4538c..a88dce2e56f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4258,14 +4258,16 @@ Keep-alive heartbeats (every 30 s by default): ### QA Recording Data Flow ``` -User asks to test → QA intent detection → CU session created with qaMode + reportToSessionId +User asks to test → QA intent detection (regex/keyword) → CU session created with qaMode +→ reportToSessionId set if conversationId available (chat context) or sourceSessionId (escalation) → macOS ScreenRecorder starts → CU action loop executes → Session terminates → ScreenRecorder stops → .mp4 saved to ~/Library/Application Support/vellum-assistant/recordings/ → cu_session_finalized sent to daemon with recording metadata -→ Daemon handler creates file-backed attachment + assistant message in source chat +→ Daemon handler creates file-backed attachment (always, for cleanup tracking) +→ If reportToSessionId present: also injects assistant message + attachment into source chat → Client loads video from GET /v1/attachments/:id/content (with Range support) → Video playable inline + draggable to Finder -→ Retention cleanup removes expired recordings after configurable period (default 7 days) +→ Retention cleanup removes expired recordings after configurable period (qaRecording.defaultRetentionDays) ``` ### File-Backed Attachment Storage From 4a1fe4f7d5b624d39a1498190f4c6b7aa388eb23 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:46:23 -0500 Subject: [PATCH 22/72] fix: recording fallback tracks all untracked recordings, not just orphans (#6960) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../__tests__/cu-session-finalized.test.ts | 85 +++++++++++++++++++ assistant/src/daemon/handlers/computer-use.ts | 11 ++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/assistant/src/__tests__/cu-session-finalized.test.ts b/assistant/src/__tests__/cu-session-finalized.test.ts index 7aa95e140fc..8480245edcd 100644 --- a/assistant/src/__tests__/cu-session-finalized.test.ts +++ b/assistant/src/__tests__/cu-session-finalized.test.ts @@ -227,6 +227,91 @@ describe('handleCuSessionFinalized', () => { }); }); + test('creates fallback attachment when reportToSessionId exists but conversation is missing and recording is present', () => { + // This tests the edge case where reportToSessionId is set but the conversation + // doesn't exist (e.g. it was deleted or never created). The recording should + // still get a file-backed attachment entry so cleanup can track it. + const ctx = makeCtx(); + const cuSessionId = 'cu-session-orphan-with-report'; + ctx.cuSessions.set(cuSessionId, {} as ComputerUseSession); + ctx.cuSessionMetadata.set(cuSessionId, { + reportToSessionId: 'nonexistent-session', + qaMode: true, + }); + + // Ensure the conversation does NOT exist. + mockConversations.delete('nonexistent-session'); + mockAddedMessages.length = 0; + + const msg: CuSessionFinalized = { + type: 'cu_session_finalized', + sessionId: cuSessionId, + status: 'completed', + summary: 'QA recording completed.', + stepCount: 3, + recording: { + localPath: '/tmp/orphan-recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 2048000, + durationMs: 45000, + width: 1920, + height: 1080, + captureScope: 'window', + includeAudio: false, + }, + }; + + // Should not throw. + handleCuSessionFinalized(msg, new net.Socket(), ctx); + + // No message persisted (conversation missing), but the recording should + // still be tracked. We can't easily verify createFileBackedAttachment + // was called without mocking it, but at minimum the function shouldn't throw + // and state should be cleaned up. + expect(mockAddedMessages.length).toBe(0); + expect(ctx.cuSessions.has(cuSessionId)).toBe(false); + expect(ctx.cuSessionMetadata.has(cuSessionId)).toBe(false); + }); + + test('creates fallback attachment when summary is empty but recording is present', () => { + const ctx = makeCtx(); + const cuSessionId = 'cu-session-empty-summary'; + ctx.cuSessions.set(cuSessionId, {} as ComputerUseSession); + ctx.cuSessionMetadata.set(cuSessionId, { + reportToSessionId: 'some-session', + qaMode: true, + }); + + mockConversations.set('some-session', { id: 'some-session' }); + mockAddedMessages.length = 0; + + const msg: CuSessionFinalized = { + type: 'cu_session_finalized', + sessionId: cuSessionId, + status: 'completed', + summary: '', // Empty summary — injection path skipped + stepCount: 2, + recording: { + localPath: '/tmp/empty-summary-recording.mp4', + mimeType: 'video/mp4', + sizeBytes: 512000, + durationMs: 10000, + width: 1280, + height: 720, + captureScope: 'display', + includeAudio: true, + }, + }; + + handleCuSessionFinalized(msg, new net.Socket(), ctx); + + // No message persisted (empty summary skips injection path). + expect(mockAddedMessages.length).toBe(0); + // State cleaned up. + expect(ctx.cuSessions.has(cuSessionId)).toBe(false); + expect(ctx.cuSessionMetadata.has(cuSessionId)).toBe(false); + }); + test('stores and retrieves CU session metadata', () => { const ctx = makeCtx(); diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 43e0694e50d..a31363fb3db 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -217,6 +217,7 @@ export function handleCuSessionFinalized( ctx: HandlerContext, ): void { const meta = ctx.cuSessionMetadata.get(msg.sessionId); + let recordingTracked = false; log.info( { @@ -264,6 +265,7 @@ export function handleCuSessionFinalized( expiresAt: msg.recording.expiresAt, }); linkAttachmentToMessage(persistedMessage.id, attachment.id, 0); + recordingTracked = true; recordingAttachment = { id: attachment.id, filename: attachment.originalFilename, @@ -332,9 +334,10 @@ export function handleCuSessionFinalized( } } - // Create a file-backed attachment for recordings without a reporting session - // so cleanup can track orphan files. - if (msg.recording && !(meta?.reportToSessionId)) { + // Create a fallback file-backed attachment for any recording that wasn't + // already tracked (no reportToSessionId, missing conversation, empty summary, + // or attachment creation failure) so cleanup can track the file on disk. + if (msg.recording && !recordingTracked) { try { createFileBackedAttachment({ filename: `qa-recording-${msg.sessionId}.mp4`, @@ -344,7 +347,7 @@ export function handleCuSessionFinalized( sha256: undefined, expiresAt: msg.recording.expiresAt, }); - log.info({ sessionId: msg.sessionId }, 'Created orphan file-backed attachment for cleanup tracking (no reportToSessionId)'); + log.info({ sessionId: msg.sessionId }, 'Created fallback file-backed attachment for cleanup tracking'); } catch (err) { log.error({ err, sessionId: msg.sessionId }, 'Failed to create file-backed attachment for orphan recording'); } From 09fea78ad4d8b12cd6b9b5618373dc79fb6e2ca5 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 12:59:17 -0500 Subject: [PATCH 23/72] feat: wire captureScope and includeAudio from daemon config to client (#6967) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/config/defaults.ts | 2 ++ assistant/src/config/schema.ts | 8 ++++++++ assistant/src/daemon/handlers/misc.ts | 2 ++ assistant/src/daemon/handlers/shared.ts | 2 ++ assistant/src/daemon/ipc-contract.ts | 4 ++++ .../vellum-assistant/App/AppDelegate+Sessions.swift | 8 ++++++-- .../macos/vellum-assistant/ComputerUse/Session.swift | 12 ++++++++++-- .../shared/IPC/Generated/IPCContractGenerated.swift | 4 ++++ 8 files changed, 38 insertions(+), 4 deletions(-) diff --git a/assistant/src/config/defaults.ts b/assistant/src/config/defaults.ts index 4bd080e809f..d694078bf4e 100644 --- a/assistant/src/config/defaults.ts +++ b/assistant/src/config/defaults.ts @@ -255,6 +255,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 f1d2ba486b1..5dc35119b9a 100644 --- a/assistant/src/config/schema.ts +++ b/assistant/src/config/schema.ts @@ -1071,6 +1071,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({ @@ -1389,6 +1395,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 21e85130d1a..88c048dac02 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -1574,6 +1574,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 RideShotgunProgress { 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 8c49793940a..ce8ff83e297 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -3446,6 +3446,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. From 8b86cf0cc8ba3ddda4e6a3a407c85614e0207fe2 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 13:05:58 -0500 Subject: [PATCH 24/72] fix: apply captureScope config to resolve window/display ID for recording (#6968) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../ComputerUse/Session.swift | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 08c126bc51c..47125ccfd06 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -143,8 +143,31 @@ 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: self.includeAudio) - log.info("QA mode: screen recording started for session \(self.id)") + var windowID: CGWindowID? + var displayID: CGDirectDisplayID? + if captureScope == "window" { + // Resolve the frontmost window's CGWindowID for window-scoped capture. + if let frontApp = NSWorkspace.shared.frontmostApplication { + let pid = frontApp.processIdentifier + if let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] { + for info in windowList { + if let ownerPID = info[kCGWindowOwnerPID as String] as? pid_t, + ownerPID == pid, + let wid = info[kCGWindowNumber as String] as? CGWindowID { + windowID = wid + break + } + } + } + } + if windowID == nil { + log.warning("QA mode: captureScope is 'window' but no frontmost window found — falling back to display capture") + } + } else { + displayID = CGMainDisplayID() + } + try await recorder.startRecording(windowID: windowID, displayID: displayID, includeAudio: self.includeAudio) + log.info("QA mode: screen recording started for session \(self.id) (scope: \(self.captureScope))") } catch { log.error("QA mode: failed to start screen recording: \(error.localizedDescription)") // Non-fatal — continue the session without recording From 251c116c67ace8f30ccef73eeefb607eb5b128b6 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 13:16:26 -0500 Subject: [PATCH 25/72] fix: exclude own PID from window capture to avoid recording Vellum overlay (#6975) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../ComputerUse/Session.swift | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 47125ccfd06..91f1f2c5e67 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -146,22 +146,23 @@ final class ComputerUseSession: ObservableObject { var windowID: CGWindowID? var displayID: CGDirectDisplayID? if captureScope == "window" { - // Resolve the frontmost window's CGWindowID for window-scoped capture. - if let frontApp = NSWorkspace.shared.frontmostApplication { - let pid = frontApp.processIdentifier - if let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] { - for info in windowList { - if let ownerPID = info[kCGWindowOwnerPID as String] as? pid_t, - ownerPID == pid, - let wid = info[kCGWindowNumber as String] as? CGWindowID { - windowID = wid - break - } + // Resolve a target window's CGWindowID for window-scoped capture. + // Exclude our own PID so we never accidentally record Vellum's + // window or overlay (which may still be frontmost in direct-start flow). + let myPID = ProcessInfo.processInfo.processIdentifier + if let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] { + for info in windowList { + if let ownerPID = info[kCGWindowOwnerPID as String] as? pid_t, + ownerPID != myPID, + let layer = info[kCGWindowLayer as String] as? Int, layer == 0, + let wid = info[kCGWindowNumber as String] as? CGWindowID { + windowID = wid + break } } } if windowID == nil { - log.warning("QA mode: captureScope is 'window' but no frontmost window found — falling back to display capture") + log.warning("QA mode: captureScope is 'window' but no suitable target window found — falling back to display capture") } } else { displayID = CGMainDisplayID() From 7811e1ce69f5dd2002738032163c0b2076518967 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 13:21:23 -0500 Subject: [PATCH 26/72] fix: narrow QA intent regex + disarm safety-net via Task.isCancelled (#6978) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/qa-intent.ts | 16 +++++++++------- .../vellum-assistant/ComputerUse/Session.swift | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/assistant/src/daemon/qa-intent.ts b/assistant/src/daemon/qa-intent.ts index 45114c8f6ed..3c878885f21 100644 --- a/assistant/src/daemon/qa-intent.ts +++ b/assistant/src/daemon/qa-intent.ts @@ -5,16 +5,18 @@ export function detectQaIntent(taskText: string): boolean { const lower = taskText.toLowerCase().trim(); - // Direct QA/test commands - if (/^(qa|test|verify|check)\b/.test(lower)) return true; + // Direct QA/test commands — "check" excluded because it's too common + // in everyday tasks ("check my email", "check the weather"). + if (/^(qa|test|verify)\b/.test(lower)) return true; - // Natural language QA patterns + // Natural language QA patterns — intentionally narrow to avoid false positives. const qaPatterns = [ - /\b(run|do|perform|execute)\s+(a\s+)?(qa|test|check|verification)\b/, - /\b(test|qa|verify|check)\s+(this|the|that|my)\b/, - /\bhelp\s+me\s+(test|qa|verify|check)\b/, - /\b(can you|could you|please)\s+(test|qa|verify|check)\b/, + /\b(run|do|perform|execute)\s+(a\s+)?(qa|test|verification)\b/, + /\b(test|qa|verify)\s+(this|the|that|my)\b/, + /\bhelp\s+me\s+(test|qa|verify)\b/, + /\b(can you|could you|please)\s+(test|qa|verify)\b/, /\btesting\s+(the|this|that|my)\b/, + /\bcheck\s+for\s+(bugs|errors|issues|regressions)\b/, ]; return qaPatterns.some(p => p.test(lower)); diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 91f1f2c5e67..7aab5bf78fd 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -1099,6 +1099,7 @@ final class ComputerUseSession: ObservableObject { // run() never reaches the post-loop block (e.g., throws or gets stuck). cancelSafetyNetTask = Task { @MainActor in try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + guard !Task.isCancelled else { return } // disarmed by run() post-loop guard self.isCancelled else { return } // in case state changed try? self.daemonClient.send(CuSessionAbortMessage(sessionId: self.id)) } From f414d5bef3988382790a6d291ee13c6575a0fe2c Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 23 Feb 2026 16:24:35 -0500 Subject: [PATCH 27/72] feat: show toast when CU ends without success - Add computerUseEndMessage(for:) to produce user-facing explanations for failed, cancelled, warning, or unexpected CU session endings - Show error toast after overlay closes in both regular and QA CU flows Co-Authored-By: Claude --- .../App/AppDelegate+Sessions.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index ab4cba8a86d..7403f518d26 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -6,6 +6,54 @@ private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum. extension AppDelegate { + // MARK: - Computer Use Post-Run UX + + /// Return a user-facing explanation when CU ended without a successful completion/response. + /// This avoids the "silent disappear" feeling when the overlay auto-closes. + private func computerUseEndMessage(for state: SessionState) -> String? { + func completionLooksUnsuccessful(_ text: String) -> Bool { + let lower = text.lowercased() + let signals = [ + "wasn't able", + "couldn't", + "could not", + "unable", + "permission denied", + "denied", + "not able", + "failed", + ] + return signals.contains { lower.contains($0) } + } + + func compact(_ text: String) -> String { + let singleLine = text.replacingOccurrences(of: "\n", with: " ") + if singleLine.count <= 220 { return singleLine } + return String(singleLine.prefix(220)) + "..." + } + + switch state { + case .completed(let summary, _): + if completionLooksUnsuccessful(summary) { + return "Computer control finished with warnings: \(compact(summary))" + } + return nil + case .responded(let answer, _): + if completionLooksUnsuccessful(answer) { + return "Computer control finished with warnings: \(compact(answer))" + } + return nil + case .failed(let reason): + return "Computer control stopped: \(reason)" + case .cancelled: + return "Computer control was cancelled." + case .awaitingConfirmation(let reason): + return "Computer control stopped while waiting for confirmation: \(reason)" + case .running, .thinking, .paused, .idle: + return "Computer control ended unexpectedly before finishing the task." + } + } + // MARK: - Accessibility Permission /// Poll for accessibility permission after prompting, giving the user time to grant it in System Settings. @@ -83,6 +131,7 @@ extension AppDelegate { } await session.run() + let endMessage = self.computerUseEndMessage(for: session.state) try? await Task.sleep(nanoseconds: 10_000_000_000) overlay.close() self.overlayWindow = nil @@ -92,6 +141,9 @@ extension AppDelegate { if mainWindowWasVisible { self.mainWindow?.show() } + if let endMessage { + self.mainWindow?.windowState.showToast(message: endMessage, style: .error) + } } } @@ -220,11 +272,15 @@ extension AppDelegate { self.overlayWindow = overlay self.ambientAgent.pause() await session.run() + let endMessage = self.computerUseEndMessage(for: session.state) try? await Task.sleep(nanoseconds: 10_000_000_000) overlay.close() self.overlayWindow = nil self.currentSession = nil self.ambientAgent.resume() + if let endMessage { + self.mainWindow?.windowState.showToast(message: endMessage, style: .error) + } default: // text_qa let session = TextSession( From 32d1f181eb2d35472b4b111ccefa0a3193d7fccd Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 23 Feb 2026 18:28:17 -0500 Subject: [PATCH 28/72] feat: target-app scoping, QA routing, and CU overlay permissions - Add target-app-hints module to resolve app constraints from task text - Scope CU sessions to target app; block Slack/Notion confusion for Vellum - Add TARGET APP CONSTRAINT section to CU system prompt - Route explicit UI/QA requests directly to computer use via shouldRouteQaToComputerUse - Extend detectQaIntent with "i want to / let's" patterns - Surface tool permission prompts (Allow/Deny) in CU overlay instead of native notifications - Keep main window visible during escalated and QA CU sessions - Dynamic overlay width based on content length; remove line limits - Handle early send failures gracefully with proper .failed state + cleanup - Add tests for QA intent detection and routing Co-Authored-By: Claude --- assistant/src/__tests__/qa-intent.test.ts | 29 +++++++ assistant/src/config/computer-use-prompt.ts | 10 +++ assistant/src/daemon/computer-use-session.ts | 79 ++++++++++++++++- assistant/src/daemon/handlers/computer-use.ts | 3 + assistant/src/daemon/handlers/misc.ts | 20 ++++- assistant/src/daemon/handlers/shared.ts | 3 + assistant/src/daemon/ipc-contract.ts | 4 + assistant/src/daemon/qa-intent.ts | 51 +++++++++++ assistant/src/daemon/target-app-hints.ts | 28 ++++++ .../App/AppDelegate+Sessions.swift | 18 ++-- .../vellum-assistant/App/AppDelegate.swift | 29 +++++-- .../ComputerUse/Session.swift | 83 ++++++++++++++++++ .../Features/Session/SessionOverlayView.swift | 87 +++++++++++++++++-- .../Session/SessionOverlayWindow.swift | 16 ++-- 14 files changed, 429 insertions(+), 31 deletions(-) create mode 100644 assistant/src/__tests__/qa-intent.test.ts create mode 100644 assistant/src/daemon/target-app-hints.ts diff --git a/assistant/src/__tests__/qa-intent.test.ts b/assistant/src/__tests__/qa-intent.test.ts new file mode 100644 index 00000000000..4bdaafe9186 --- /dev/null +++ b/assistant/src/__tests__/qa-intent.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'bun:test'; +import { detectQaIntent, shouldRouteQaToComputerUse } from '../daemon/qa-intent.js'; + +describe('detectQaIntent', () => { + test('matches natural language QA phrasing', () => { + expect(detectQaIntent('Hey assistant, can you help me test this behavior')).toBe(true); + expect(detectQaIntent('I want to QA the vellum desktop app')).toBe(true); + }); + + test('does not match unrelated check requests', () => { + expect(detectQaIntent('Can you check my email?')).toBe(false); + }); +}); + +describe('shouldRouteQaToComputerUse', () => { + test('routes explicit UI/app QA requests to computer use', () => { + expect( + shouldRouteQaToComputerUse( + 'I want to QA the vellum desktop app and test out when a user types 2 lines in the composer', + ), + ).toBe(true); + expect(shouldRouteQaToComputerUse('Please test this workflow by clicking Send in the app')).toBe(true); + }); + + test('does not force computer use for code-test requests', () => { + expect(shouldRouteQaToComputerUse('Please write integration tests for this API handler')).toBe(false); + expect(shouldRouteQaToComputerUse('Can you add unit tests using vitest for this util')).toBe(false); + }); +}); diff --git a/assistant/src/config/computer-use-prompt.ts b/assistant/src/config/computer-use-prompt.ts index 2943b1fe10b..eeb9651e44c 100644 --- a/assistant/src/config/computer-use-prompt.ts +++ b/assistant/src/config/computer-use-prompt.ts @@ -18,12 +18,22 @@ function currentDateString(): string { export function buildComputerUseSystemPrompt( screenWidth: number, screenHeight: number, + targetAppName?: string, + targetAppBundleId?: string, ): string { const dateStr = currentDateString(); + const targetAppSection = targetAppName + ? ` +TARGET APP CONSTRAINT: +- This task is scoped to app "${targetAppName}"${targetAppBundleId ? ` (bundle id: ${targetAppBundleId})` : ''}. +- Do NOT interpret similarly named workspaces, channels, or documents in other apps as the target app. +- Do NOT switch to other apps unless the user explicitly requested a cross-app workflow.` + : ''; return `You are vellum-assistant's computer use agent. You control the user's Mac to accomplish tasks. The screen is ${screenWidth}\u00d7${screenHeight} pixels. +${targetAppSection} ACTION EXECUTION HIERARCHY: Not all actions require the same execution method. Always prefer the least invasive, most reliable approach. Foreground computer use (clicking, typing, scrolling) takes over the user's cursor and keyboard — it is disruptive and should be your LAST resort, not your first instinct. diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 49bcbe73083..a592ab77dc1 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -60,6 +60,8 @@ export class ComputerUseSession { private readonly interactionType: 'computer_use' | 'text_qa'; private readonly onTerminal?: (sessionId: string) => void; private readonly preactivatedSkillIds: string[]; + private readonly targetAppName?: string; + private readonly targetAppBundleId?: string; private readonly skillProjectionState = new Map(); private readonly skillProjectionCache: SkillProjectionCache = {}; @@ -95,6 +97,8 @@ export class ComputerUseSession { interactionType?: 'computer_use' | 'text_qa', onTerminal?: (sessionId: string) => void, preactivatedSkillIds?: string[], + targetAppName?: string, + targetAppBundleId?: string, ) { this.sessionId = sessionId; this.task = task; @@ -105,6 +109,8 @@ export class ComputerUseSession { this.interactionType = interactionType ?? 'computer_use'; this.onTerminal = onTerminal; this.preactivatedSkillIds = preactivatedSkillIds ?? ['computer-use']; + this.targetAppName = targetAppName; + this.targetAppBundleId = targetAppBundleId; } // --------------------------------------------------------------------------- @@ -222,6 +228,29 @@ export class ComputerUseSession { return this.state; } + private isTargetAppMatch(candidateAppName: string): boolean { + if (!this.targetAppName) return true; + const candidate = ComputerUseSession.normalizeAppLabel(candidateAppName); + const target = ComputerUseSession.normalizeAppLabel(this.targetAppName); + if (!candidate || !target) return true; + return candidate === target || target.includes(candidate) || candidate.includes(target); + } + + private extractAppleScriptActivationTarget(script: string): string | undefined { + const match = /tell\s+application\s+"([^"]+)"\s+to\s+activate/i.exec(script); + return match?.[1]; + } + + private static normalizeAppLabel(value: string): string { + return value.toLowerCase().replace(/[^a-z0-9]/g, ''); + } + + private shouldBlockKnownVellumConfusionApp(candidateAppName: string): boolean { + if (this.targetAppBundleId !== 'com.vellum.vellum-assistant') return false; + const candidate = ComputerUseSession.normalizeAppLabel(candidateAppName); + return candidate.includes('slack') || candidate.includes('notion'); + } + /** * Compute CU tool definitions from the bundled computer-use skill via * skill projection. Returns null if projection fails so the caller can @@ -278,7 +307,12 @@ export class ComputerUseSession { // --------------------------------------------------------------------------- private async runAgentLoop(messages: Message[]): Promise { - const systemPrompt = buildComputerUseSystemPrompt(this.screenWidth, this.screenHeight); + const systemPrompt = buildComputerUseSystemPrompt( + this.screenWidth, + this.screenHeight, + this.targetAppName, + this.targetAppBundleId, + ); let cuToolDefs = this.getProjectedCuToolDefinitions(); if (!cuToolDefs) { @@ -421,6 +455,38 @@ export class ComputerUseSession { }); } + // Guard app switching when this session has an explicit target app. + if (toolName === 'computer_use_open_app') { + const requestedApp = + (typeof input.app_name === 'string' ? input.app_name : undefined) + ?? (typeof input.appName === 'string' ? input.appName : undefined); + if ( + requestedApp + && !this.isTargetAppMatch(requestedApp) + && this.shouldBlockKnownVellumConfusionApp(requestedApp) + ) { + return { + content: `Blocked: this task is scoped to "${this.targetAppName}". Do not switch to "${requestedApp}" unless the user explicitly requests a cross-app workflow.`, + isError: true, + }; + } + } + + if (toolName === 'computer_use_run_applescript') { + const script = typeof input.script === 'string' ? input.script : undefined; + const activatedApp = script ? this.extractAppleScriptActivationTarget(script) : undefined; + if ( + activatedApp + && !this.isTargetAppMatch(activatedApp) + && this.shouldBlockKnownVellumConfusionApp(activatedApp) + ) { + return { + content: `Blocked: this task is scoped to "${this.targetAppName}". AppleScript cannot activate "${activatedApp}" unless the user explicitly requests a cross-app workflow.`, + isError: true, + }; + } + } + // ── Computer-use tool proxying ───────────────────────────────── const reasoning = typeof input.reasoning === 'string' ? input.reasoning : undefined; @@ -747,6 +813,17 @@ export class ComputerUseSession { // Text block const textParts: string[] = []; + if (this.targetAppName) { + textParts.push(`TARGET APP: ${this.targetAppName}`); + if (this.targetAppBundleId) { + textParts.push(`TARGET BUNDLE ID: ${this.targetAppBundleId}`); + } + textParts.push( + 'Constraint: Treat similarly named workspaces/documents in other apps as unrelated. Stay in the target app unless the user explicitly asks for a cross-app workflow.', + ); + textParts.push(''); + } + const trimmedTask = this.task.trim(); if (trimmedTask) { textParts.push(`TASK: ${trimmedTask}`); diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index a31363fb3db..dabb5baf7b5 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -93,6 +93,9 @@ export function handleCuSessionCreate( sendToClient, msg.interactionType, onTerminal, + undefined, + msg.targetAppName, + msg.targetAppBundleId, ); sessionRef.current = session; diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index 02d5e83f1fd..6bfc56420b5 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -20,7 +20,8 @@ import type { } from '../ipc-protocol.js'; import { log, wireEscalationHandler, renderHistoryContent, defineHandlers, type HandlerContext } from './shared.js'; import { handleCuSessionCreate } from './computer-use.js'; -import { detectQaIntent } from '../qa-intent.js'; +import { detectQaIntent, shouldRouteQaToComputerUse } from '../qa-intent.js'; +import { resolveComputerUseTargetAppHint } from '../target-app-hints.js'; // ─── Task submit handler ──────────────────────────────────────────────────── @@ -67,15 +68,25 @@ export async function handleTaskSubmit( // Slash candidates always route to text_qa — bypass classifier const slashCandidate = parseSlashCandidate(msg.task); + const isQa = detectQaIntent(msg.task); + const forceQaComputerUse = shouldRouteQaToComputerUse(msg.task); const interactionType = slashCandidate.kind === 'candidate' ? 'text_qa' as const - : await classifyInteraction(msg.task, msg.source); - rlog.info({ interactionType, slashBypass: slashCandidate.kind === 'candidate', taskLength: msg.task.length }, 'Task classified'); + : forceQaComputerUse + ? 'computer_use' as const + : await classifyInteraction(msg.task, msg.source); + rlog.info({ + interactionType, + slashBypass: slashCandidate.kind === 'candidate', + taskLength: msg.task.length, + isQa, + forceQaComputerUse, + }, 'Task classified'); if (interactionType === 'computer_use') { // Create CU session (reuse handleCuSessionCreate logic) const sessionId = uuid(); - const isQa = detectQaIntent(msg.task); + const targetApp = resolveComputerUseTargetAppHint(msg.task); const config = getConfig(); const cuMsg: CuSessionCreate = { type: 'cu_session_create', @@ -85,6 +96,7 @@ export async function handleTaskSubmit( screenHeight: msg.screenHeight, attachments: msg.attachments, interactionType: 'computer_use', + ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), ...(isQa ? { qaMode: true, reportToSessionId: msg.conversationId } : {}), }; handleCuSessionCreate(cuMsg, socket, ctx); diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index 9a461b16358..8bb37bbbcee 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -10,6 +10,7 @@ import type { SecretPromptResult } from '../../permissions/secret-prompter.js'; import { getConfig } from '../../config/loader.js'; import type { DebouncerMap } from '../../util/debounce.js'; import { detectQaIntent } from '../qa-intent.js'; +import { resolveComputerUseTargetAppHint } from '../target-app-hints.js'; const log = getLogger('handlers'); @@ -237,6 +238,7 @@ export function wireEscalationHandler( const cuSessionId = uuid(); const isQa = detectQaIntent(task); + const targetApp = resolveComputerUseTargetAppHint(task); const config = getConfig(); const cuMsg: CuSessionCreate = { type: 'cu_session_create', @@ -246,6 +248,7 @@ export function wireEscalationHandler( screenHeight, interactionType: 'computer_use', reportToSessionId: sourceSessionId, + ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), ...(isQa ? { qaMode: true } : {}), }; handleCuSessionCreate(cuMsg, currentSocket, ctx); diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 88c048dac02..cd96272f7d6 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -181,6 +181,10 @@ export interface CuSessionCreate { reportToSessionId?: string; /** Marks this CU run as a QA/test workflow. */ qaMode?: boolean; + /** Optional target app name constraint for disambiguation. */ + targetAppName?: string; + /** Optional target app bundle identifier for disambiguation. */ + targetAppBundleId?: string; } export interface CuSessionAbort { diff --git a/assistant/src/daemon/qa-intent.ts b/assistant/src/daemon/qa-intent.ts index 3c878885f21..9066b1a0a71 100644 --- a/assistant/src/daemon/qa-intent.ts +++ b/assistant/src/daemon/qa-intent.ts @@ -13,6 +13,7 @@ export function detectQaIntent(taskText: string): boolean { const qaPatterns = [ /\b(run|do|perform|execute)\s+(a\s+)?(qa|test|verification)\b/, /\b(test|qa|verify)\s+(this|the|that|my)\b/, + /\b(i want to|let'?s)\s+(qa|test|verify)\b/, /\bhelp\s+me\s+(test|qa|verify)\b/, /\b(can you|could you|please)\s+(test|qa|verify)\b/, /\btesting\s+(the|this|that|my)\b/, @@ -21,3 +22,53 @@ export function detectQaIntent(taskText: string): boolean { return qaPatterns.some(p => p.test(lower)); } + +/** + * Whether a QA/test request should be routed directly to foreground computer use. + * This is used to avoid classifier drift for explicit UI/app QA requests. + */ +export function shouldRouteQaToComputerUse(taskText: string): boolean { + if (!detectQaIntent(taskText)) return false; + + const lower = taskText.toLowerCase().trim(); + const guiCues = [ + /\bdesktop app\b/, + /\bapp\b/, + /\bui\b/, + /\bscreen\b/, + /\bwindow\b/, + /\bcomposer\b/, + /\bthread\b/, + /\bchat\b/, + /\bbutton\b/, + /\bclick\b/, + /\btype\b/, + /\btyping\b/, + /\bscroll\b/, + /\bnavigate\b/, + /\bopen\b/, + /\bsend\b/, + /\bworkflow\b/, + /\bbehavior\b/, + ]; + const codeTestCues = [ + /\bunit tests?\b/, + /\bintegration tests?\b/, + /\be2e tests?\b/, + /\bwrite tests?\b/, + /\btest file\b/, + /\bjest\b/, + /\bvitest\b/, + /\bpytest\b/, + /\bmocha\b/, + /\bcypress\b/, + /\bplaywright\b/, + /\bci\b/, + ]; + + const hasGuiCue = guiCues.some((pattern) => pattern.test(lower)); + if (hasGuiCue) return true; + + const hasCodeOnlyCue = codeTestCues.some((pattern) => pattern.test(lower)); + return !hasCodeOnlyCue; +} diff --git a/assistant/src/daemon/target-app-hints.ts b/assistant/src/daemon/target-app-hints.ts new file mode 100644 index 00000000000..64425afc91b --- /dev/null +++ b/assistant/src/daemon/target-app-hints.ts @@ -0,0 +1,28 @@ +export interface ComputerUseTargetAppHint { + appName: string; + bundleId?: string; +} + +/** + * Resolve an explicit target app hint from user task text. + * This is intentionally conservative: only high-confidence patterns should + * lock the CU session to an app. + */ +export function resolveComputerUseTargetAppHint(task: string): ComputerUseTargetAppHint | undefined { + const normalized = task.toLowerCase(); + + // "Vellum app"/"Velly app"/"Vellum assistant" should target the desktop app, + // not similarly named Slack workspaces or Notion pages. + const vellumDesktopMentioned = + /\b(vellum|velly)\s+(desktop\s+)?app\b/.test(normalized) + || /\b(vellum|velly)\s+assistant\b/.test(normalized); + + if (vellumDesktopMentioned) { + return { + appName: 'Vellum Assistant', + bundleId: 'com.vellum.vellum-assistant', + }; + } + + return undefined; +} diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 7403f518d26..246b7aa578f 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -124,11 +124,9 @@ extension AppDelegate { self.textResponseWindow?.close() self.textResponseWindow = nil - // Hide main window so the target app becomes frontmost for CU - let mainWindowWasVisible = self.mainWindow?.isVisible ?? false - if mainWindowWasVisible { - self.mainWindow?.hide() - } + // Keep the main app visible during escalated CU so permission prompts + // and status are always visible to the user. + self.showMainWindow() await session.run() let endMessage = self.computerUseEndMessage(for: session.state) @@ -138,9 +136,6 @@ extension AppDelegate { self.currentSession = nil self.currentTextSession = nil self.ambientAgent.resume() - if mainWindowWasVisible { - self.mainWindow?.show() - } if let endMessage { self.mainWindow?.windowState.showToast(message: endMessage, style: .error) } @@ -271,6 +266,13 @@ extension AppDelegate { overlay.show() self.overlayWindow = overlay self.ambientAgent.pause() + let looksLikeQaTask = effectiveTask.localizedCaseInsensitiveContains("qa") + || effectiveTask.localizedCaseInsensitiveContains("test") + || effectiveTask.localizedCaseInsensitiveContains("verify") + if routed.qaMode == true || looksLikeQaTask { + // QA/test CU sessions should keep the main app open. + self.showMainWindow() + } await session.run() let endMessage = self.computerUseEndMessage(for: session.state) try? await Task.sleep(nanoseconds: 10_000_000_000) diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 3d107d9866d..351b58b6859 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -876,18 +876,35 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { return } + // Active CU sessions do not have reliable inline-chat confirmation UX. + // Always surface permission prompts directly in the CU overlay. + if let currentSession = self.currentSession { + currentSession.presentToolPermissionPrompt(msg) + self.overlayWindow?.bringToFront() + return + } + // When the chat window is visible AND the confirmation belongs to the // active thread, the inline ToolConfirmationBubble handles the // confirmation UX — skip the native notification to avoid showing a // duplicate prompt. If the confirmation is for a background thread, // the inline bubble won't be visible, so we must still fire the // native notification. - if NSApp.isActive, let mainWindow = self.mainWindow, mainWindow.isVisible { - let activeSessionId = mainWindow.threadManager.activeViewModel?.sessionId - let confirmationIsForActiveThread = msg.sessionId == nil || msg.sessionId == activeSessionId - if confirmationIsForActiveThread { - return - } + // + // Important: do NOT treat a nil sessionId as "active thread". CU + // sessions may omit the sessionId, and suppressing their prompts + // causes the permission request to time out and auto-deny silently. + // Also skip suppression entirely when a CU session is running — + // CU prompts are never handled by the inline chat bubble. + if NSApp.isActive, + let mainWindow = self.mainWindow, + mainWindow.isVisible, + let promptSessionId = msg.sessionId, + let activeSessionId = mainWindow.threadManager.activeViewModel?.sessionId, + promptSessionId == activeSessionId + { + log.info("Suppressing native notification for confirmation \(msg.requestId) — inline bubble handles it (sessionId=\(promptSessionId))") + return } let decision = await self.toolConfirmationNotificationService.showConfirmation(msg) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 7aab5bf78fd..6120bb01732 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -18,11 +18,19 @@ enum SessionState: Equatable { case cancelled } +struct PendingToolPermissionPrompt: Equatable { + let requestId: String + let toolName: String + let riskLevel: String + let summary: String +} + @MainActor final class ComputerUseSession: ObservableObject { @Published var state: SessionState = .idle @Published var undoCount = 0 @Published var autoApproveTools = false + @Published var pendingToolPermissionPrompt: PendingToolPermissionPrompt? let task: String let id: String @@ -123,6 +131,7 @@ final class ComputerUseSession: ObservableObject { verifier.reset() isCancelled = false isPaused = false + pendingToolPermissionPrompt = nil previousAXTreeText = nil previousElements = nil previousFlatElements = nil @@ -205,6 +214,15 @@ final class ComputerUseSession: ObservableObject { )) } catch { log.error("Failed to send session create message: \(error)") + state = .failed(reason: "Assistant connection lost before session start") + logger.finishSession(result: "failed: session_create send failed") + // Disarm cancel safety net — this run is terminating early. + cancelSafetyNetTask?.cancel() + cancelSafetyNetTask = nil + if qaMode { + await finalizeQARecording() + } + return } } @@ -215,6 +233,15 @@ final class ComputerUseSession: ObservableObject { try daemonClient.send(obs) } catch { log.error("Failed to send initial observation: \(error)") + state = .failed(reason: "Assistant connection lost before first action") + logger.finishSession(result: "failed: initial observation send failed") + // Disarm cancel safety net — this run is terminating early. + cancelSafetyNetTask?.cancel() + cancelSafetyNetTask = nil + if qaMode { + await finalizeQARecording() + } + return } } else { state = .failed(reason: "No focused window and screen capture failed") @@ -1092,6 +1119,7 @@ final class ComputerUseSession: ObservableObject { messageLoopTask?.cancel() confirmationContinuation?.resume(returning: false) confirmationContinuation = nil + pendingToolPermissionPrompt = nil if qaMode { // Deferred abort: give run() a chance to send finalization first, @@ -1132,4 +1160,59 @@ final class ComputerUseSession: ObservableObject { } } } + + func presentToolPermissionPrompt(_ message: ConfirmationRequestMessage) { + pendingToolPermissionPrompt = PendingToolPermissionPrompt( + requestId: message.requestId, + toolName: message.toolName, + riskLevel: message.riskLevel, + summary: Self.summarizeToolPermissionInput(message.input) + ) + log.info("CU prompt surfaced in overlay: requestId=\(message.requestId, privacy: .public), tool=\(message.toolName, privacy: .public)") + } + + func approveToolPermissionPrompt() { + respondToToolPermissionPrompt(decision: "allow") + } + + func denyToolPermissionPrompt() { + respondToToolPermissionPrompt(decision: "deny") + } + + private func respondToToolPermissionPrompt(decision: String) { + guard let pending = pendingToolPermissionPrompt else { return } + do { + try daemonClient.send(ConfirmationResponseMessage( + requestId: pending.requestId, + decision: decision + )) + pendingToolPermissionPrompt = nil + log.info("CU prompt resolved in overlay: requestId=\(pending.requestId, privacy: .public), decision=\(decision, privacy: .public)") + } catch { + log.error("Failed to send CU tool confirmation response: \(error.localizedDescription)") + } + } + + private static func summarizeToolPermissionInput(_ input: [String: AnyCodable]) -> String { + if let appName = input["appName"]?.value as? String, !appName.isEmpty { + return "Target app: \(appName)" + } + if let reasoning = input["reasoning"]?.value as? String, !reasoning.isEmpty { + return reasoning + } + if let command = input["command"]?.value as? String, !command.isEmpty { + return command + } + if input.isEmpty { + return "No additional details provided." + } + let text = input + .sorted { $0.key < $1.key } + .map { key, value in + let rendered = value.value.map { String(describing: $0) } ?? "null" + return "\(key)=\(rendered)" + } + .joined(separator: ", ") + return text.count > 220 ? String(text.prefix(220)) + "..." : text + } } diff --git a/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift b/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift index 45baaf3de8c..98d3cd37413 100644 --- a/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift +++ b/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift @@ -4,6 +4,41 @@ import SwiftUI struct SessionOverlayView: View { @ObservedObject var session: ComputerUseSession + private let minOverlayWidth: CGFloat = 340 + private let maxOverlayWidth: CGFloat = 560 + + private var overlayWidth: CGFloat { + let length = longestVisibleTextLength + if length > 220 { return maxOverlayWidth } + if length > 140 { return 500 } + if length > 90 { return 420 } + return minOverlayWidth + } + + private var longestVisibleTextLength: Int { + var candidates: [String] = [session.task] + if let prompt = session.pendingToolPermissionPrompt { + candidates.append(prompt.toolName) + candidates.append(prompt.summary) + } + switch session.state { + case .running(_, _, let lastAction, let reasoning): + candidates.append(lastAction) + candidates.append(reasoning) + case .awaitingConfirmation(let reason): + candidates.append(reason) + case .completed(let summary, _): + candidates.append(summary) + case .responded(let answer, _): + candidates.append(answer) + case .failed(let reason): + candidates.append(reason) + case .idle, .thinking, .paused, .cancelled: + break + } + return candidates.map(\.count).max() ?? 0 + } + var body: some View { VStack(alignment: .leading, spacing: VSpacing.md + VSpacing.xxs) { // Header @@ -19,10 +54,15 @@ struct SessionOverlayView: View { Text(session.task) .font(VFont.caption) .foregroundStyle(.secondary) - .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) Divider() + if let prompt = session.pendingToolPermissionPrompt { + toolPermissionPromptView(prompt) + Divider() + } + // State-dependent content stateContent @@ -30,7 +70,41 @@ struct SessionOverlayView: View { controlButtons } .padding(14) - .frame(width: 340, alignment: .leading) + .frame(width: overlayWidth, alignment: .leading) + } + + private func toolPermissionPromptView(_ prompt: PendingToolPermissionPrompt) -> some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 4) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.yellow) + Text("Permission needed") + .font(.caption.bold()) + } + + Text("Tool: \(prompt.toolName) (\(prompt.riskLevel))") + .font(.caption) + .foregroundStyle(.primary) + + Text(prompt.summary) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 8) { + Button("Allow") { + session.approveToolPermissionPrompt() + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .controlSize(.small) + + Button("Deny") { + session.denyToolPermissionPrompt() + } + .controlSize(.small) + } + } } @ViewBuilder @@ -67,14 +141,14 @@ struct SessionOverlayView: View { Text(reasoning) .font(.caption) .foregroundStyle(.primary) - .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) .padding(.leading, 6) } } Text(lastAction) .font(.caption) .foregroundStyle(.secondary) - .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) } case .paused(let step, let maxSteps): @@ -126,7 +200,7 @@ struct SessionOverlayView: View { Text(summary) .font(.caption) .foregroundStyle(.secondary) - .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) } } @@ -146,7 +220,6 @@ struct SessionOverlayView: View { } .frame(maxHeight: 200) } - .frame(width: 380) case .failed(let reason): HStack(spacing: 6) { @@ -155,7 +228,7 @@ struct SessionOverlayView: View { Text(reason) .font(.caption) .foregroundStyle(.secondary) - .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) } case .cancelled: diff --git a/clients/macos/vellum-assistant/Features/Session/SessionOverlayWindow.swift b/clients/macos/vellum-assistant/Features/Session/SessionOverlayWindow.swift index c60136f02ec..90b6af1bbf6 100644 --- a/clients/macos/vellum-assistant/Features/Session/SessionOverlayWindow.swift +++ b/clients/macos/vellum-assistant/Features/Session/SessionOverlayWindow.swift @@ -6,7 +6,7 @@ import SwiftUI final class SessionOverlayWindow { private var panel: NSPanel? private let session: ComputerUseSession - private var stateCancellable: AnyCancellable? + private var layoutCancellable: AnyCancellable? init(session: ComputerUseSession) { self.session = session @@ -34,19 +34,25 @@ final class SessionOverlayWindow { // Size window to fit SwiftUI content and resize on every state change sizeAndPosition(panel) - stateCancellable = session.$state + layoutCancellable = session.objectWillChange .sink { [weak self, weak panel] _ in guard let self, let panel else { return } - self.sizeAndPosition(panel) + DispatchQueue.main.async { + self.sizeAndPosition(panel) + } } panel.orderFront(nil) self.panel = panel } + func bringToFront() { + panel?.orderFrontRegardless() + } + func close() { - stateCancellable?.cancel() - stateCancellable = nil + layoutCancellable?.cancel() + layoutCancellable = nil panel?.close() panel = nil } From ac326ea30c02e241c625155e651e39fcf0478d0d Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:30:13 -0500 Subject: [PATCH 29/72] feat: strict target-app guard + hardened openApp resolution (#7281) Replace narrow Slack/Notion confusion blocking with fail-closed non-target blocking in CU sessions. When a target app is set, ALL non-matching open_app and run_applescript activations are blocked unless the user's original task text explicitly requests cross-app work (e.g. "copy from Chrome and paste into Vellum"). Harden openApp resolution: bundle-id first, then fuzzy name match, then alias table, then filesystem search. Add Vellum naming variant aliases ("Vellum" / "Velly" -> "Vellum Assistant"). Return structured error messages (app_not_found / app_mismatch) to prevent click-fallback drift. Log resolution path for observability. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 51 +++++++++--- .../ComputerUse/ActionExecutor.swift | 78 ++++++++++++++++--- 2 files changed, 110 insertions(+), 19 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index a592ab77dc1..cadb2906eab 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -245,10 +245,27 @@ export class ComputerUseSession { return value.toLowerCase().replace(/[^a-z0-9]/g, ''); } - private shouldBlockKnownVellumConfusionApp(candidateAppName: string): boolean { - if (this.targetAppBundleId !== 'com.vellum.vellum-assistant') return false; - const candidate = ComputerUseSession.normalizeAppLabel(candidateAppName); - return candidate.includes('slack') || candidate.includes('notion'); + /** + * Returns true when the original user task text explicitly requests a + * cross-app workflow (e.g. "copy from Chrome and paste into Vellum"). + * Only the user's original task counts — model-generated reasoning + * about switching apps does NOT qualify as an escape. + */ + private taskExplicitlyRequestsCrossApp(): boolean { + if (!this.task) return false; + const t = this.task.toLowerCase(); + // Matches patterns like "copy from X", "paste into Y", "switch to Z", + // "open X and Y", "drag from X to Y", "move … to ", etc. + const crossAppPatterns = [ + /\bcopy\s+from\s+\w+.*\bpaste\s+(in|into|to)\b/, + /\bswitch\s+to\s+\w+/, + /\bopen\s+\w+.*\band\s+(then\s+)?open\b/, + /\bdrag\s+from\s+\w+.*\bto\s+\w+/, + /\bmove\s+.*\bto\s+\w+/, + /\bfrom\s+\w+.*\b(into|to)\s+\w+/, + /\buse\s+\w+.*\band\s+\w+/, + ]; + return crossAppPatterns.some((p) => p.test(t)); } /** @@ -455,7 +472,9 @@ export class ComputerUseSession { }); } - // Guard app switching when this session has an explicit target app. + // Fail-closed guard: when a target app is set, block ALL non-matching + // open_app and run_applescript activations. The only escape is when the + // user's original task text explicitly requests a cross-app workflow. if (toolName === 'computer_use_open_app') { const requestedApp = (typeof input.app_name === 'string' ? input.app_name : undefined) @@ -463,10 +482,17 @@ export class ComputerUseSession { if ( requestedApp && !this.isTargetAppMatch(requestedApp) - && this.shouldBlockKnownVellumConfusionApp(requestedApp) + && !this.taskExplicitlyRequestsCrossApp() ) { + log.warn({ + sessionId: this.sessionId, + toolName, + requestedApp, + targetApp: this.targetAppName, + crossAppEscapeChecked: true, + }, 'Blocked non-target app activation'); return { - content: `Blocked: this task is scoped to "${this.targetAppName}". Do not switch to "${requestedApp}" unless the user explicitly requests a cross-app workflow.`, + content: `Blocked: this task is scoped to "${this.targetAppName}". Do not switch to "${requestedApp}". Only the user can authorize cross-app workflows.`, isError: true, }; } @@ -478,10 +504,17 @@ export class ComputerUseSession { if ( activatedApp && !this.isTargetAppMatch(activatedApp) - && this.shouldBlockKnownVellumConfusionApp(activatedApp) + && !this.taskExplicitlyRequestsCrossApp() ) { + log.warn({ + sessionId: this.sessionId, + toolName, + requestedApp: activatedApp, + targetApp: this.targetAppName, + crossAppEscapeChecked: true, + }, 'Blocked non-target AppleScript activation'); return { - content: `Blocked: this task is scoped to "${this.targetAppName}". AppleScript cannot activate "${activatedApp}" unless the user explicitly requests a cross-app workflow.`, + content: `Blocked: this task is scoped to "${this.targetAppName}". AppleScript cannot activate "${activatedApp}". Only the user can authorize cross-app workflows.`, isError: true, }; } diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index d9c315ef535..3f5e3c7ade6 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -1,6 +1,12 @@ import CoreGraphics import AppKit import ApplicationServices +import os + +private let log = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", + category: "ActionExecutor" +) enum ExecutorError: LocalizedError { case eventCreationFailed @@ -10,6 +16,7 @@ enum ExecutorError: LocalizedError { case unknownKey(String) case accessibilityNotGranted case appNotFound(String) + case appMismatch(requested: String, resolved: String) case appleScriptError(String) case appleScriptMissingScript case appleScriptTimeout @@ -22,7 +29,8 @@ enum ExecutorError: LocalizedError { case .missingKey: return "Key action requires key name" case .unknownKey(let key): return "Unknown key: \(key)" case .accessibilityNotGranted: return "Accessibility permission not granted" - case .appNotFound(let name): return "Application not found: \(name)" + case .appNotFound(let name): return "app_not_found: \(name)" + case .appMismatch(let requested, let resolved): return "app_mismatch: requested=\(requested) resolved=\(resolved)" case .appleScriptError(let msg): return "AppleScript error: \(msg)" case .appleScriptMissingScript: return "run_applescript requires a script" case .appleScriptTimeout: return "AppleScript timed out after 5 seconds" @@ -330,25 +338,72 @@ final class ActionExecutor: ActionExecuting { "outlook": "Microsoft Outlook", "teams": "Microsoft Teams", "iterm": "iTerm", + "vellum": "Vellum Assistant", + "velly": "Vellum Assistant", ] - func openApp(name: String) async throws { + /// Strips non-alphanumeric characters and lowercases for fuzzy comparison. + private static func normalizeAppName(_ value: String) -> String { + value.lowercased().filter { $0.isLetter || $0.isNumber } + } + + func openApp(name: String, bundleId: String? = nil) async throws { let workspace = NSWorkspace.shared - // 1. Check running apps for exact or case-insensitive match - let nameLower = name.lowercased() - if let runningApp = workspace.runningApplications.first(where: { - $0.localizedName?.lowercased() == nameLower + // 1. Bundle-ID resolution — most precise, avoids name ambiguity + if let bundleId, !bundleId.isEmpty { + if let runningApp = workspace.runningApplications.first(where: { + $0.bundleIdentifier == bundleId + }) { + log.info("openApp resolved via bundle-id (running): \(bundleId, privacy: .public)") + runningApp.activate() + try await Task.sleep(nanoseconds: 300_000_000) + return + } + if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleId) { + log.info("openApp resolved via bundle-id (installed): \(bundleId, privacy: .public)") + let config = NSWorkspace.OpenConfiguration() + config.activates = true + try await workspace.openApplication(at: appURL, configuration: config) + return + } + log.warning("openApp bundle-id not found, falling back to name: \(bundleId, privacy: .public)") + } + + // 2. Normalized/fuzzy name matching against running apps + let normalizedName = Self.normalizeAppName(name) + if let runningApp = workspace.runningApplications.first(where: { app in + guard let localizedName = app.localizedName else { return false } + let normalized = Self.normalizeAppName(localizedName) + return normalized == normalizedName + || normalized.contains(normalizedName) + || normalizedName.contains(normalized) }) { + log.info("openApp resolved via fuzzy name match (running): \(name, privacy: .public) -> \(runningApp.localizedName ?? "?", privacy: .public)") runningApp.activate() - try await Task.sleep(nanoseconds: 300_000_000) // 300ms for app to come forward + try await Task.sleep(nanoseconds: 300_000_000) return } - // 2. Resolve aliases + // 3. Resolve aliases + let nameLower = name.lowercased() let resolvedName = Self.appAliases[nameLower] ?? name - // 3. Search common application directories + if resolvedName != name { + log.info("openApp resolved via alias: \(name, privacy: .public) -> \(resolvedName, privacy: .public)") + // Re-check running apps with the aliased name + let normalizedResolved = Self.normalizeAppName(resolvedName) + if let runningApp = workspace.runningApplications.first(where: { app in + guard let localizedName = app.localizedName else { return false } + return Self.normalizeAppName(localizedName) == normalizedResolved + }) { + runningApp.activate() + try await Task.sleep(nanoseconds: 300_000_000) + return + } + } + + // 4. Filesystem search in common application directories let searchDirs = [ "/Applications", "/System/Applications", @@ -360,6 +415,7 @@ final class ActionExecutor: ActionExecuting { let appPath = "\(dir)/\(resolvedName).app" let appURL = URL(fileURLWithPath: appPath) if FileManager.default.fileExists(atPath: appPath) { + log.info("openApp resolved via filesystem (exact): \(appPath, privacy: .public)") let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) @@ -367,18 +423,20 @@ final class ActionExecutor: ActionExecuting { } } - // 4. Try case-insensitive filesystem search in /Applications + // 5. Case-insensitive filesystem search in /Applications if let found = try? FileManager.default.contentsOfDirectory(atPath: "/Applications") .first(where: { $0.lowercased() == "\(resolvedName.lowercased()).app" }) { let appURL = URL(fileURLWithPath: "/Applications/\(found)") + log.info("openApp resolved via filesystem (case-insensitive): \(found, privacy: .public)") let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) return } + log.error("openApp failed: app_not_found name=\(name, privacy: .public) bundleId=\(bundleId ?? "nil", privacy: .public)") throw ExecutorError.appNotFound(name) } From 4ae4f4c3e936d4dfc38fe91a4355fe323473ab5f Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:31:07 -0500 Subject: [PATCH 30/72] feat: propagate target app through IPC + frontmost-app runtime guard (#7282) Add targetAppName and targetAppBundleId to the TaskRouted IPC contract so the Swift client knows which app a CU session should be targeting. Both direct-CU and escalation routing paths now include these fields. On the Swift side, ComputerUseSession stores the target app and checks it before every destructive action (click, type, key, scroll, drag). On mismatch, it attempts a single activation retry (300ms) and blocks with a clear error observation if the wrong app is still in front. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/handlers/misc.ts | 1 + assistant/src/daemon/handlers/shared.ts | 1 + assistant/src/daemon/ipc-contract.ts | 4 ++ .../App/AppDelegate+Sessions.swift | 8 ++- .../ComputerUse/Session.swift | 71 ++++++++++++++++++- .../IPC/Generated/IPCContractGenerated.swift | 8 +++ 6 files changed, 90 insertions(+), 3 deletions(-) diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index 6bfc56420b5..40baeee4d84 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -105,6 +105,7 @@ export async function handleTaskSubmit( type: 'task_routed', sessionId, interactionType: 'computer_use', + ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), ...(isQa ? { qaMode: true, reportToSessionId: msg.conversationId, diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index 8bb37bbbcee..78c8d757ed1 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -260,6 +260,7 @@ export function wireEscalationHandler( task, escalatedFrom: sourceSessionId, reportToSessionId: sourceSessionId, + ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), ...(isQa ? { qaMode: true, retentionDays: config.qaRecording.defaultRetentionDays, diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index cd96272f7d6..f81ffca2ba5 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -1582,6 +1582,10 @@ export interface TaskRouted { captureScope?: 'window' | 'display'; /** Whether to include audio in QA recording (from daemon config). */ includeAudio?: boolean; + /** Target app name for frontmost-app guard (from target-app-hints). */ + targetAppName?: string; + /** Target app bundle ID for frontmost-app guard (from target-app-hints). */ + targetAppBundleId?: string; } export interface RideShotgunProgress { diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 246b7aa578f..b3cdb06645d 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -107,7 +107,9 @@ extension AppDelegate { qaMode: routed.qaMode ?? false, retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, captureScope: routed.captureScope ?? "display", - includeAudio: routed.includeAudio ?? false + includeAudio: routed.includeAudio ?? false, + targetAppName: routed.targetAppName, + targetAppBundleId: routed.targetAppBundleId ) // Don't bind relatedViewModel for escalated sessions — the active view model // may be unrelated if the user switched threads. Tool calls for escalated @@ -256,7 +258,9 @@ extension AppDelegate { qaMode: routed.qaMode ?? false, retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, captureScope: routed.captureScope ?? "display", - includeAudio: routed.includeAudio ?? false + includeAudio: routed.includeAudio ?? false, + targetAppName: routed.targetAppName, + targetAppBundleId: routed.targetAppBundleId ) // 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 6120bb01732..87200b7cedd 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -55,6 +55,11 @@ final class ComputerUseSession: ObservableObject { /// Whether to include audio in QA recording (from daemon config, default false). let includeAudio: Bool + /// Target app name for frontmost-app guard — nil means no constraint. + let targetAppName: String? + /// Target app bundle ID for frontmost-app guard — nil means no constraint. + let targetAppBundleId: String? + /// Weak reference to the chat view model for extracting tool calls for notifications. weak var relatedViewModel: ChatViewModel? @@ -102,7 +107,9 @@ final class ComputerUseSession: ObservableObject { qaMode: Bool = false, retentionDays: Int = 7, captureScope: String = "display", - includeAudio: Bool = false + includeAudio: Bool = false, + targetAppName: String? = nil, + targetAppBundleId: String? = nil ) { self.id = sessionId ?? UUID().uuidString self.task = task @@ -123,6 +130,8 @@ final class ComputerUseSession: ObservableObject { self.retentionDays = retentionDays self.captureScope = captureScope self.includeAudio = includeAudio + self.targetAppName = targetAppName + self.targetAppBundleId = targetAppBundleId self.verifier = ActionVerifier(maxSteps: maxSteps) self.logger = SessionLogger(task: task, attachments: attachments) } @@ -465,6 +474,23 @@ final class ComputerUseSession: ObservableObject { return } + // FRONTMOST-APP GUARD — block destructive actions when the wrong app is in front. + // Non-destructive actions (screenshot, open_app, wait, runAppleScript) are allowed + // regardless of which app is focused. + if let guardError = await checkFrontmostAppGuard(for: agentAction) { + log.error("Frontmost guard BLOCKED action \(agentAction.type.rawValue): \(guardError)") + let obs = await buildObservation(executionResult: nil, executionError: guardError) + if let obs { + do { + try daemonClient.send(obs) + } catch { + log.error("Failed to send frontmost-guard blocked observation: \(error)") + } + } + state = .thinking(step: action.stepNumber + 1, maxSteps: maxSteps) + return + } + // EXECUTE var executionResult: String? = nil var executionError: String? = nil @@ -510,6 +536,49 @@ final class ComputerUseSession: ObservableObject { state = .thinking(step: action.stepNumber + 1, maxSteps: maxSteps) } + // MARK: - Frontmost App Guard + + /// Returns an error message if the frontmost app doesn't match the target and + /// activation retry failed; returns nil if the action should proceed normally. + private func checkFrontmostAppGuard(for action: AgentAction) async -> String? { + let destructiveTypes: Set = [.click, .doubleClick, .rightClick, .type, .key, .scroll, .drag] + guard destructiveTypes.contains(action.type) else { return nil } + + // When we have a bundle ID, match on that (most reliable) + if let targetBundleId = targetAppBundleId { + let frontmost = NSWorkspace.shared.frontmostApplication + let frontmostBundleId = frontmost?.bundleIdentifier + let frontmostName = frontmost?.localizedName ?? "unknown" + + guard frontmostBundleId != targetBundleId else { return nil } + + log.warning("Frontmost guard: frontmost=\(frontmostName) (\(frontmostBundleId ?? "nil")), target=\(self.targetAppName ?? "nil") (\(targetBundleId)). Attempting activation retry.") + + // Attempt to activate the target app once + if let targetRunning = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == targetBundleId }) { + targetRunning.activate() + try? await Task.sleep(nanoseconds: 300_000_000) // 300ms for focus to settle + if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == targetBundleId { + log.info("Frontmost guard: activation retry succeeded for \(self.targetAppName ?? targetBundleId)") + return nil + } + } + + return "Action blocked: frontmost app is '\(frontmostName)' but target is '\(self.targetAppName ?? targetBundleId)'. Please switch to the target app first." + } + + // Fall back to name-based matching when no bundle ID is available + if let targetName = targetAppName { + let frontmostName = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + guard frontmostName != targetName else { return nil } + + log.warning("Frontmost guard (name): frontmost=\(frontmostName), target=\(targetName). No bundle ID for activation retry.") + return "Action blocked: frontmost app is '\(frontmostName)' but target is '\(targetName)'. Please switch to the target app first." + } + + return nil + } + // MARK: - Observation Builder private func buildObservation(executionResult: String?, executionError: String?) async -> CuObservationMessage? { diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index ce8ff83e297..96dc95c06cd 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -925,6 +925,10 @@ public struct IPCCuSessionCreate: Codable, Sendable { public let reportToSessionId: String? /// Marks this CU run as a QA/test workflow. public let qaMode: Bool? + /// Optional target app name constraint for disambiguation. + public let targetAppName: String? + /// Optional target app bundle identifier for disambiguation. + public let targetAppBundleId: String? } public struct IPCCuSessionFinalized: Codable, Sendable { @@ -3450,6 +3454,10 @@ public struct IPCTaskRouted: Codable, Sendable { public let captureScope: String? /// Whether to include audio in QA recording (from daemon config). public let includeAudio: Bool? + /// Target app name for frontmost-app guard (from target-app-hints). + public let targetAppName: String? + /// Target app bundle ID for frontmost-app guard (from target-app-hints). + public let targetAppBundleId: String? } /// Server push — broadcast when a task run creates a conversation, so the client can show it as a chat thread. From b33896824c24d77b19bfda8f64cf3d712d944fd0 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:36:05 -0500 Subject: [PATCH 31/72] feat: proactive daemon-side auto-approve for CU sessions (#7288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `cu_auto_approve_update` IPC message so the daemon knows when the client toggles auto-approve. When enabled, the daemon's PermissionPrompter proxy skips the client round-trip for low/medium risk tools — returning `{ decision: 'allow' }` immediately. High-risk tools always require manual approval regardless of auto-approve state. Client-side: normalize risk level with `.lowercased()` before comparing, send IPC update on toggle change, and retro-resolve any pending low/medium-risk prompt when auto-approve is toggled ON. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../__snapshots__/ipc-snapshot.test.ts.snap | 28 +++++++++------ assistant/src/__tests__/ipc-snapshot.test.ts | 5 +++ assistant/src/daemon/computer-use-session.ts | 35 +++++++++++++++++-- assistant/src/daemon/handlers/computer-use.ts | 19 ++++++++++ .../src/daemon/ipc-contract-inventory.json | 2 ++ assistant/src/daemon/ipc-contract.ts | 7 ++++ .../vellum-assistant/App/AppDelegate.swift | 4 ++- .../ComputerUse/Session.swift | 28 ++++++++++++++- .../IPC/Generated/IPCContractGenerated.swift | 6 ++++ clients/shared/IPC/IPCMessages.swift | 10 ++++++ 10 files changed, 130 insertions(+), 14 deletions(-) diff --git a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap index 2ff5da1bd86..e33f9cee353 100644 --- a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +++ b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap @@ -146,6 +146,24 @@ exports[`IPC message snapshots ClientMessage types cu_session_abort serializes t } `; +exports[`IPC message snapshots ClientMessage types cu_auto_approve_update serializes to expected JSON 1`] = ` +{ + "enabled": true, + "sessionId": "cu-sess-001", + "type": "cu_auto_approve_update", +} +`; + +exports[`IPC message snapshots ClientMessage types cu_session_finalized serializes to expected JSON 1`] = ` +{ + "sessionId": "cu-sess-001", + "status": "completed", + "stepCount": 5, + "summary": "Task completed successfully", + "type": "cu_session_finalized", +} +`; + exports[`IPC message snapshots ClientMessage types cu_observation serializes to expected JSON 1`] = ` { "axDiff": "+ new element", @@ -2499,13 +2517,3 @@ exports[`IPC message snapshots ServerMessage types tool_names_list_response seri "type": "tool_names_list_response", } `; - -exports[`IPC message snapshots ClientMessage types cu_session_finalized serializes to expected JSON 1`] = ` -{ - "sessionId": "cu-sess-001", - "status": "completed", - "stepCount": 5, - "summary": "Task completed successfully", - "type": "cu_session_finalized", -} -`; diff --git a/assistant/src/__tests__/ipc-snapshot.test.ts b/assistant/src/__tests__/ipc-snapshot.test.ts index a0e59b7067c..51a7ffee363 100644 --- a/assistant/src/__tests__/ipc-snapshot.test.ts +++ b/assistant/src/__tests__/ipc-snapshot.test.ts @@ -104,6 +104,11 @@ const clientMessages: Record = { type: 'cu_session_abort', sessionId: 'cu-sess-001', }, + cu_auto_approve_update: { + type: 'cu_auto_approve_update', + sessionId: 'cu-sess-001', + enabled: true, + }, cu_session_finalized: { type: 'cu_session_finalized', sessionId: 'cu-sess-001', diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index cadb2906eab..4af59c9c664 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -84,6 +84,9 @@ export class ComputerUseSession { private terminalNotified = false; private prompter: PermissionPrompter | null = null; + /** When true, low/medium-risk tool prompts are auto-approved without client round-trip. */ + autoApproveEnabled = false; + // Tracks the agent loop promise so callers can await session completion private loopPromise: Promise | null = null; @@ -359,9 +362,37 @@ export class ComputerUseSession { ]; this.prompter = new PermissionPrompter(this.sendToClient); - const prompter = this.prompter; + const innerPrompter = this.prompter; const secretPrompter = new SecretPrompter(this.sendToClient); - const executor = new ToolExecutor(prompter); + + // Wrap the prompter so low/medium-risk tools are auto-approved when + // the client has toggled auto-approve on, avoiding the IPC round-trip. + const sessionRef = this; + const autoApprovePrompter = new Proxy(innerPrompter, { + get(target, prop, receiver) { + if (prop === 'prompt') { + return async function ( + toolName: string, + input: Record, + riskLevel: string, + ...rest: unknown[] + ) { + const normalizedRisk = riskLevel.toLowerCase(); + if (sessionRef.autoApproveEnabled && (normalizedRisk === 'low' || normalizedRisk === 'medium')) { + log.info( + { sessionId: sessionRef.sessionId, toolName, riskLevel: normalizedRisk }, + 'Auto-approved tool prompt (proactive, skipping client round-trip)', + ); + return { decision: 'allow' as const }; + } + return (target.prompt as Function).call(target, toolName, input, riskLevel, ...rest); + }; + } + return Reflect.get(target, prop, receiver); + }, + }) as PermissionPrompter; + + const executor = new ToolExecutor(autoApprovePrompter); const proxyResolver = async ( toolName: string, diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index dabb5baf7b5..838ff6b2713 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -7,6 +7,7 @@ import { readBlob, deleteBlob, validateBlobKindEncoding } from '../ipc-blob-stor import type { CuSessionCreate, CuSessionAbort, + CuAutoApproveUpdate, CuSessionFinalized, CuObservation, ServerMessage, @@ -363,9 +364,27 @@ export function handleCuSessionFinalized( ctx.cuSessionMetadata.delete(msg.sessionId); } +export function handleCuAutoApproveUpdate( + msg: CuAutoApproveUpdate, + _socket: net.Socket, + ctx: HandlerContext, +): void { + const session = ctx.cuSessions.get(msg.sessionId); + if (!session) { + log.debug({ sessionId: msg.sessionId }, 'CU auto-approve update: session not found (already finished?)'); + return; + } + session.autoApproveEnabled = msg.enabled; + log.info( + { sessionId: msg.sessionId, autoApproveEnabled: msg.enabled }, + 'CU session auto-approve state changed', + ); +} + export const computerUseHandlers = defineHandlers({ cu_session_create: handleCuSessionCreate, cu_session_abort: handleCuSessionAbort, + cu_auto_approve_update: handleCuAutoApproveUpdate, cu_session_finalized: handleCuSessionFinalized, cu_observation: handleCuObservation, }); diff --git a/assistant/src/daemon/ipc-contract-inventory.json b/assistant/src/daemon/ipc-contract-inventory.json index 4d113dd3501..2ea7f979709 100644 --- a/assistant/src/daemon/ipc-contract-inventory.json +++ b/assistant/src/daemon/ipc-contract-inventory.json @@ -20,6 +20,7 @@ "BundleAppRequest", "CancelRequest", "ConfirmationResponse", + "CuAutoApproveUpdate", "CuObservation", "CuSessionAbort", "CuSessionCreate", @@ -273,6 +274,7 @@ "bundle_app", "cancel", "confirmation_response", + "cu_auto_approve_update", "cu_observation", "cu_session_abort", "cu_session_create", diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index f81ffca2ba5..c2f346a8804 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -192,6 +192,12 @@ export interface CuSessionAbort { sessionId: string; } +export interface CuAutoApproveUpdate { + type: 'cu_auto_approve_update'; + sessionId: string; + enabled: boolean; +} + export interface CuSessionFinalized { type: 'cu_session_finalized'; sessionId: string; @@ -1078,6 +1084,7 @@ export type ClientMessage = | SandboxSetRequest | CuSessionCreate | CuSessionAbort + | CuAutoApproveUpdate | CuSessionFinalized | CuObservation | RideShotgunStart diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 351b58b6859..ca55b9ef49a 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -859,8 +859,10 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { guard let self else { return } Task { @MainActor in // Auto-approve low/medium risk tool confirmations during CU sessions + // (reactive fallback — daemon-side proactive skip handles most cases) + let normalizedRisk = msg.riskLevel.lowercased() if self.currentSession?.autoApproveTools == true, - msg.riskLevel == "low" || msg.riskLevel == "medium" { + normalizedRisk == "low" || normalizedRisk == "medium" { do { try self.daemonClient.sendConfirmationResponse( requestId: msg.requestId, diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 87200b7cedd..4d0c810db38 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -29,7 +29,33 @@ struct PendingToolPermissionPrompt: Equatable { final class ComputerUseSession: ObservableObject { @Published var state: SessionState = .idle @Published var undoCount = 0 - @Published var autoApproveTools = false + @Published var autoApproveTools = false { + didSet { + guard autoApproveTools != oldValue else { return } + log.info("Auto-approve tools toggled: \(autoApproveTools)") + + // Notify daemon so it can skip prompt creation proactively + do { + try daemonClient.send(CuAutoApproveUpdateMessage(sessionId: id, enabled: autoApproveTools)) + } catch { + log.error("Failed to send cu_auto_approve_update: \(error)") + } + + // Retro-resolve: if toggling ON and a low/medium-risk prompt is pending, approve it immediately + if autoApproveTools, let pending = pendingToolPermissionPrompt { + let normalizedRisk = pending.riskLevel.lowercased() + if normalizedRisk == "low" || normalizedRisk == "medium" { + log.info("Retro-resolving pending prompt \(pending.requestId) (tool=\(pending.toolName), risk=\(normalizedRisk))") + do { + try daemonClient.send(ConfirmationResponseMessage(requestId: pending.requestId, decision: "allow")) + } catch { + log.error("Failed to retro-resolve pending prompt: \(error)") + } + pendingToolPermissionPrompt = nil + } + } + } + } @Published var pendingToolPermissionPrompt: PendingToolPermissionPrompt? let task: String diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 96dc95c06cd..3c9a2e8ffd7 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -831,6 +831,12 @@ public struct IPCCuAction: Codable, Sendable { } } +public struct IPCCuAutoApproveUpdate: Codable, Sendable { + public let type: String + public let sessionId: String + public let enabled: Bool +} + public struct IPCCuComplete: Codable, Sendable { public let type: String public let sessionId: String diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index dc70873c2b7..de8730aaaf1 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -342,6 +342,16 @@ extension IPCCuSessionAbort { } } +/// Sent to notify the daemon of auto-approve toggle changes for a CU session. +/// Backed by generated `IPCCuAutoApproveUpdate`. +public typealias CuAutoApproveUpdateMessage = IPCCuAutoApproveUpdate + +extension IPCCuAutoApproveUpdate { + public init(sessionId: String, enabled: Bool) { + self.init(type: "cu_auto_approve_update", sessionId: sessionId, enabled: enabled) + } +} + /// Authenticate to the daemon on initial socket connect. /// Backed by generated `IPCAuthMessage`. public typealias AuthMessage = IPCAuthMessage From ed4859eef196fad230e2d8d89d19fc324c4c0a33 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:41:13 -0500 Subject: [PATCH 32/72] fix: preserve pending prompt on retro-approve send failure (#7290) Only clear pendingToolPermissionPrompt after a successful daemon send in the autoApproveTools didSet observer. Previously, the prompt was cleared unconditionally after the do/catch block, so if the send failed the user lost the ability to manually approve. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- clients/macos/vellum-assistant/ComputerUse/Session.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 4d0c810db38..adb45cd2ca2 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -48,10 +48,10 @@ final class ComputerUseSession: ObservableObject { log.info("Retro-resolving pending prompt \(pending.requestId) (tool=\(pending.toolName), risk=\(normalizedRisk))") do { try daemonClient.send(ConfirmationResponseMessage(requestId: pending.requestId, decision: "allow")) + pendingToolPermissionPrompt = nil } catch { - log.error("Failed to retro-resolve pending prompt: \(error)") + log.error("Failed to retro-resolve pending prompt: \(error) — keeping prompt visible for manual approval") } - pendingToolPermissionPrompt = nil } } } From 0b04313e64f7ec8efb916e24f85168d7ee1d7807 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:41:17 -0500 Subject: [PATCH 33/72] fix: handle legacy denied screen recording on first request (#7291) For users who denied screen recording before the hasRequestedBefore flag existed, CGRequestScreenCaptureAccess() is a no-op on upgrade. Previously the first click did nothing because the code set the flag and skipped opening System Settings. Now we check CGPreflightScreenCaptureAccess() after the request call and fall back to System Settings if still denied. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- clients/macos/vellum-assistant/App/PermissionManager.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/macos/vellum-assistant/App/PermissionManager.swift b/clients/macos/vellum-assistant/App/PermissionManager.swift index f80f43c7d8d..eb0d938d164 100644 --- a/clients/macos/vellum-assistant/App/PermissionManager.swift +++ b/clients/macos/vellum-assistant/App/PermissionManager.swift @@ -33,6 +33,12 @@ enum PermissionManager { if !hasRequestedBefore { UserDefaults.standard.set(true, forKey: hasRequestedScreenRecordingFlag) + // For legacy installs that denied screen recording before this flag + // existed: CGRequestScreenCaptureAccess() was a no-op, so check if + // permission is still denied and fall back to System Settings. + if CGPreflightScreenCaptureAccess() == false { + openScreenRecordingSettings() + } } else if !CGPreflightScreenCaptureAccess() { openScreenRecordingSettings() } From 0d40ca5e857188bc34c36479c975ec68f24d650c Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:42:06 -0500 Subject: [PATCH 34/72] fix: tighten cross-app escape hatch + guard empty fuzzy match (#7292) Replace overly broad regex patterns in taskExplicitlyRequestsCrossApp() with app-name-aware detection that requires two distinct known app names to be mentioned. The old patterns matched single-app actions like "switch to dark mode" or "move the file to trash". Guard fuzzy app matching against empty normalized input to prevent String.contains("") from matching the first running app arbitrarily when input like "---" normalizes to an empty string. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 57 +++++++++++--- .../ComputerUse/ActionExecutor.swift | 4 +- .../IPC/Generated/IPCContractGenerated.swift | 77 ++++++++++++++++++- 3 files changed, 123 insertions(+), 15 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 4af59c9c664..ae824a18147 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -248,27 +248,60 @@ export class ComputerUseSession { return value.toLowerCase().replace(/[^a-z0-9]/g, ''); } + /** + * Well-known app names used to detect cross-app intent in task text. + * Only needs to cover apps commonly referenced in cross-app workflows; + * the list does not need to be exhaustive. + */ + private static readonly KNOWN_APP_NAMES: ReadonlySet = new Set([ + 'chrome', 'google chrome', 'safari', 'firefox', 'arc', 'brave', 'edge', + 'slack', 'discord', 'zoom', 'teams', 'microsoft teams', + 'notion', 'obsidian', 'bear', 'notes', 'apple notes', + 'finder', 'terminal', 'iterm', 'iterm2', 'warp', + 'vscode', 'visual studio code', 'cursor', 'xcode', 'intellij', 'webstorm', + 'figma', 'sketch', 'photoshop', 'illustrator', + 'mail', 'outlook', 'gmail', 'thunderbird', + 'spotify', 'music', 'apple music', + 'messages', 'imessage', 'whatsapp', 'telegram', 'signal', + 'calendar', 'reminders', 'todoist', 'things', + 'pages', 'numbers', 'keynote', 'word', 'excel', 'powerpoint', + 'preview', 'acrobat', 'pdf expert', + 'vellum', 'vellum assistant', + 'linear', 'jira', 'github', 'gitlab', + 'postman', 'docker', 'tableplus', 'sequel pro', + 'system preferences', 'system settings', 'activity monitor', + ]); + /** * Returns true when the original user task text explicitly requests a * cross-app workflow (e.g. "copy from Chrome and paste into Vellum"). * Only the user's original task counts — model-generated reasoning * about switching apps does NOT qualify as an escape. + * + * Detection strategy: check whether the task text mentions at least two + * different known app names. This avoids false positives from generic + * phrases like "switch to dark mode" or "move the file to trash". */ private taskExplicitlyRequestsCrossApp(): boolean { if (!this.task) return false; const t = this.task.toLowerCase(); - // Matches patterns like "copy from X", "paste into Y", "switch to Z", - // "open X and Y", "drag from X to Y", "move … to ", etc. - const crossAppPatterns = [ - /\bcopy\s+from\s+\w+.*\bpaste\s+(in|into|to)\b/, - /\bswitch\s+to\s+\w+/, - /\bopen\s+\w+.*\band\s+(then\s+)?open\b/, - /\bdrag\s+from\s+\w+.*\bto\s+\w+/, - /\bmove\s+.*\bto\s+\w+/, - /\bfrom\s+\w+.*\b(into|to)\s+\w+/, - /\buse\s+\w+.*\band\s+\w+/, - ]; - return crossAppPatterns.some((p) => p.test(t)); + + // Collect distinct app names mentioned in the task text. + const mentionedApps = new Set(); + for (const appName of ComputerUseSession.KNOWN_APP_NAMES) { + // Word-boundary check: the app name must appear as a standalone word/phrase, + // not as a substring of another word. + const escaped = appName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (new RegExp(`\\b${escaped}\\b`).test(t)) { + // Normalize to a canonical form so e.g. "google chrome" and "chrome" + // are counted as the same app. + const canonical = ComputerUseSession.normalizeAppLabel(appName); + mentionedApps.add(canonical); + } + // Early exit once we confirm at least two distinct apps. + if (mentionedApps.size >= 2) return true; + } + return false; } /** diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index 3f5e3c7ade6..d103f1103c1 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -372,7 +372,9 @@ final class ActionExecutor: ActionExecuting { // 2. Normalized/fuzzy name matching against running apps let normalizedName = Self.normalizeAppName(name) - if let runningApp = workspace.runningApplications.first(where: { app in + // Guard: when the input normalizes to empty (e.g. "---"), String.contains("") + // returns true for any string, which would match the first running app arbitrarily. + if !normalizedName.isEmpty, let runningApp = workspace.runningApplications.first(where: { app in guard let localizedName = app.localizedName else { return false } let normalized = Self.normalizeAppName(localizedName) return normalized == normalizedName diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 3c9a2e8ffd7..15516c4c411 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -835,6 +835,12 @@ public struct IPCCuAutoApproveUpdate: Codable, Sendable { public let type: String public let sessionId: String public let enabled: Bool + + public init(type: String, sessionId: String, enabled: Bool) { + self.type = type + self.sessionId = sessionId + self.enabled = enabled + } } public struct IPCCuComplete: Codable, Sendable { @@ -935,6 +941,20 @@ public struct IPCCuSessionCreate: Codable, Sendable { public let targetAppName: String? /// Optional target app bundle identifier for disambiguation. public let targetAppBundleId: String? + + public init(type: String, sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]?, interactionType: String?, reportToSessionId: String?, qaMode: Bool?, targetAppName: String?, targetAppBundleId: String?) { + self.type = type + self.sessionId = sessionId + self.task = task + self.screenWidth = screenWidth + self.screenHeight = screenHeight + self.attachments = attachments + self.interactionType = interactionType + self.reportToSessionId = reportToSessionId + self.qaMode = qaMode + self.targetAppName = targetAppName + self.targetAppBundleId = targetAppBundleId + } } public struct IPCCuSessionFinalized: Codable, Sendable { @@ -944,6 +964,15 @@ public struct IPCCuSessionFinalized: Codable, Sendable { public let summary: String public let stepCount: Int public let recording: IPCCuSessionFinalizedRecording? + + public init(type: String, sessionId: String, status: String, summary: String, stepCount: Int, recording: IPCCuSessionFinalizedRecording?) { + self.type = type + self.sessionId = sessionId + self.status = status + self.summary = summary + self.stepCount = stepCount + self.recording = recording + } } public struct IPCCuSessionFinalizedRecording: Codable, Sendable { @@ -957,6 +986,19 @@ public struct IPCCuSessionFinalizedRecording: Codable, Sendable { public let includeAudio: Bool public let targetBundleId: String? public let expiresAt: Int? + + public init(localPath: String, mimeType: String, sizeBytes: Int, durationMs: Int, width: Int, height: Int, captureScope: String, includeAudio: Bool, targetBundleId: String?, expiresAt: Int?) { + self.localPath = localPath + self.mimeType = mimeType + self.sizeBytes = sizeBytes + self.durationMs = durationMs + self.width = width + self.height = height + self.captureScope = captureScope + self.includeAudio = includeAudio + self.targetBundleId = targetBundleId + self.expiresAt = expiresAt + } } public struct IPCDaemonStatusMessage: Codable, Sendable { @@ -2025,7 +2067,7 @@ public struct IPCMemoryRecalled: Codable, Sendable { public let latencyMs: Int public let topCandidates: [IPCMemoryRecalledCandidateDebug] - public init(type: String, provider: String, model: String, lexicalHits: Double, semanticHits: Double, recencyHits: Double, entityHits: Double, relationSeedEntityCount: Int?, relationTraversedEdgeCount: Int?, relationNeighborEntityCount: Int?, relationExpandedItemCount: Int?, earlyTerminated: Bool?, mergedCount: Int, selectedCount: Int, rerankApplied: Bool, injectedTokens: Int, latencyMs: Double, topCandidates: [IPCMemoryRecalledCandidateDebug]) { + public init(type: String, provider: String, model: String, lexicalHits: Double, semanticHits: Double, recencyHits: Double, entityHits: Double, relationSeedEntityCount: Int?, relationTraversedEdgeCount: Int?, relationNeighborEntityCount: Int?, relationExpandedItemCount: Int?, earlyTerminated: Bool?, mergedCount: Int, selectedCount: Int, rerankApplied: Bool, injectedTokens: Int, latencyMs: Int, topCandidates: [IPCMemoryRecalledCandidateDebug]) { self.type = type self.provider = provider self.model = model @@ -2082,7 +2124,7 @@ public struct IPCMemoryStatus: Codable, Sendable { public let cleanupResolvedJobsCompleted24h: Double public let cleanupSupersededJobsCompleted24h: Double - public init(type: String, enabled: Bool, degraded: Bool, reason: String?, provider: String?, model: String?, conflictsPending: Double, conflictsResolved: Double, oldestPendingConflictAgeMs: Double?, cleanupResolvedJobsPending: Double, cleanupSupersededJobsPending: Double, cleanupResolvedJobsCompleted24h: Double, cleanupSupersededJobsCompleted24h: Double) { + public init(type: String, enabled: Bool, degraded: Bool, reason: String?, provider: String?, model: String?, conflictsPending: Double, conflictsResolved: Double, oldestPendingConflictAgeMs: Int?, cleanupResolvedJobsPending: Double, cleanupSupersededJobsPending: Double, cleanupResolvedJobsCompleted24h: Double, cleanupSupersededJobsCompleted24h: Double) { self.type = type self.enabled = enabled self.degraded = degraded @@ -2410,6 +2452,12 @@ public struct IPCRideShotgunProgress: Codable, Sendable { public let type: String public let watchId: String public let message: String + + public init(type: String, watchId: String, message: String) { + self.type = type + self.watchId = watchId + self.message = message + } } public struct IPCRideShotgunResult: Codable, Sendable { @@ -3464,6 +3512,21 @@ public struct IPCTaskRouted: Codable, Sendable { public let targetAppName: String? /// Target app bundle ID for frontmost-app guard (from target-app-hints). public let targetAppBundleId: String? + + public init(type: String, sessionId: String, interactionType: String, task: String?, escalatedFrom: String?, qaMode: Bool?, reportToSessionId: String?, retentionDays: Double?, captureScope: String?, includeAudio: Bool?, targetAppName: String?, targetAppBundleId: String?) { + self.type = type + self.sessionId = sessionId + self.interactionType = interactionType + self.task = task + self.escalatedFrom = escalatedFrom + self.qaMode = qaMode + self.reportToSessionId = reportToSessionId + self.retentionDays = retentionDays + self.captureScope = captureScope + self.includeAudio = includeAudio + self.targetAppName = targetAppName + self.targetAppBundleId = targetAppBundleId + } } /// Server push — broadcast when a task run creates a conversation, so the client can show it as a chat thread. @@ -3499,6 +3562,16 @@ public struct IPCTaskSubmit: Codable, Sendable { public let source: String? /// The originating conversation/thread ID, if submitting from a chat context. public let conversationId: String? + + public init(type: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]?, source: String?, conversationId: String?) { + self.type = type + self.task = task + self.screenWidth = screenWidth + self.screenHeight = screenHeight + self.attachments = attachments + self.source = source + self.conversationId = conversationId + } } public struct IPCTelegramConfigRequest: Codable, Sendable { From a4be32c1dd8620c4ee5cdac605ea108f243e5eb8 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 18:45:17 -0500 Subject: [PATCH 35/72] fix: frontmost guard consecutive-block termination + CuSessionCreate wrapper (#7300) Add consecutive-block counter to frontmost-app guard so sessions fail fast when the target app cannot be activated (3 consecutive blocks triggers session termination). Also fix CuSessionCreate convenience init to forward targetAppName and targetAppBundleId fields, and forward them from the session create call site. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../vellum-assistant/ComputerUse/Session.swift | 16 ++++++++++++++-- clients/shared/IPC/IPCMessages.swift | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index adb45cd2ca2..653062082ac 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -107,6 +107,7 @@ final class ComputerUseSession: ObservableObject { private var previousFlatElements: [AXElement]? private var consecutiveUnchangedSteps = 0 private var currentStepNumber = 0 + private var consecutiveFrontmostBlocks = 0 /// Adaptive delay configuration private let adaptiveDelayEnabled: Bool @@ -172,6 +173,7 @@ final class ComputerUseSession: ObservableObject { previousFlatElements = nil consecutiveUnchangedSteps = 0 currentStepNumber = 0 + consecutiveFrontmostBlocks = 0 state = .running(step: 0, maxSteps: maxSteps, lastAction: "Starting...", reasoning: "") log.info("Session starting — task: \(self.task, privacy: .public)") @@ -245,7 +247,9 @@ final class ComputerUseSession: ObservableObject { attachments: ipcAttachments, interactionType: interactionTypeString, reportToSessionId: reportToSessionId, - qaMode: qaMode ? true : nil + qaMode: qaMode ? true : nil, + targetAppName: targetAppName, + targetAppBundleId: targetAppBundleId )) } catch { log.error("Failed to send session create message: \(error)") @@ -504,7 +508,14 @@ final class ComputerUseSession: ObservableObject { // Non-destructive actions (screenshot, open_app, wait, runAppleScript) are allowed // regardless of which app is focused. if let guardError = await checkFrontmostAppGuard(for: agentAction) { - log.error("Frontmost guard BLOCKED action \(agentAction.type.rawValue): \(guardError)") + consecutiveFrontmostBlocks += 1 + log.error("Frontmost guard BLOCKED action \(agentAction.type.rawValue) (\(self.consecutiveFrontmostBlocks) consecutive): \(guardError)") + if consecutiveFrontmostBlocks >= 3 { + isCancelled = true + state = .failed(reason: "Target app could not be activated after repeated attempts.") + logger.finishSession(result: "failed: frontmost guard — too many blocks") + return + } let obs = await buildObservation(executionResult: nil, executionError: guardError) if let obs { do { @@ -516,6 +527,7 @@ final class ComputerUseSession: ObservableObject { state = .thinking(step: action.stepNumber + 1, maxSteps: maxSteps) return } + consecutiveFrontmostBlocks = 0 // EXECUTE var executionResult: String? = nil diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index de8730aaaf1..1f669ff11ae 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -148,8 +148,8 @@ extension IPCUserMessageAttachment { public typealias CuSessionCreateMessage = IPCCuSessionCreate extension IPCCuSessionCreate { - public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?, reportToSessionId: String? = nil, qaMode: Bool? = nil) { - self.init(type: "cu_session_create", sessionId: sessionId, task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, interactionType: interactionType, reportToSessionId: reportToSessionId, qaMode: qaMode) + public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?, reportToSessionId: String? = nil, qaMode: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil) { + self.init(type: "cu_session_create", sessionId: sessionId, task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, interactionType: interactionType, reportToSessionId: reportToSessionId, qaMode: qaMode, targetAppName: targetAppName, targetAppBundleId: targetAppBundleId) } } From 7a4623abff5086a9a165998ed98fd4a7c8487c6e Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:04:55 -0500 Subject: [PATCH 36/72] fix: QA auto-approve from session start + recorder salvage + toast UX (#7379) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../App/AppDelegate+Sessions.swift | 43 +++++++++++-------- .../ComputerUse/ScreenRecorder.swift | 11 ++++- .../ComputerUse/Session.swift | 34 ++++++++++++++- .../DesignSystem/Core/Feedback/VToast.swift | 7 ++- clients/shared/IPC/IPCMessages.swift | 6 +-- 5 files changed, 73 insertions(+), 28 deletions(-) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index b3cdb06645d..590059ec3cb 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -10,7 +10,9 @@ extension AppDelegate { /// Return a user-facing explanation when CU ended without a successful completion/response. /// This avoids the "silent disappear" feeling when the overlay auto-closes. - private func computerUseEndMessage(for state: SessionState) -> String? { + private func computerUseEndMessage(for session: ComputerUseSession) -> String? { + let state = session.state + func completionLooksUnsuccessful(_ text: String) -> Bool { let lower = text.lowercased() let signals = [ @@ -26,32 +28,37 @@ extension AppDelegate { return signals.contains { lower.contains($0) } } - func compact(_ text: String) -> String { - let singleLine = text.replacingOccurrences(of: "\n", with: " ") - if singleLine.count <= 220 { return singleLine } - return String(singleLine.prefix(220)) + "..." - } - - switch state { + let baseMessage: String? = switch state { case .completed(let summary, _): if completionLooksUnsuccessful(summary) { - return "Computer control finished with warnings: \(compact(summary))" + "Computer control finished with warnings: \(summary.replacingOccurrences(of: "\n", with: " "))" + } else { + nil } - return nil case .responded(let answer, _): if completionLooksUnsuccessful(answer) { - return "Computer control finished with warnings: \(compact(answer))" + "Computer control finished with warnings: \(answer.replacingOccurrences(of: "\n", with: " "))" + } else { + nil } - return nil case .failed(let reason): - return "Computer control stopped: \(reason)" + "Computer control stopped: \(reason)" case .cancelled: - return "Computer control was cancelled." + "Computer control was cancelled." case .awaitingConfirmation(let reason): - return "Computer control stopped while waiting for confirmation: \(reason)" + "Computer control stopped while waiting for confirmation: \(reason)" case .running, .thinking, .paused, .idle: - return "Computer control ended unexpectedly before finishing the task." + "Computer control ended unexpectedly before finishing the task." } + + if let recordingWarning = session.qaRecordingWarningMessage { + if let baseMessage { + return "\(baseMessage) Recording warning: \(recordingWarning)" + } + return "Computer control finished with warnings: \(recordingWarning)" + } + + return baseMessage } // MARK: - Accessibility Permission @@ -131,7 +138,7 @@ extension AppDelegate { self.showMainWindow() await session.run() - let endMessage = self.computerUseEndMessage(for: session.state) + let endMessage = self.computerUseEndMessage(for: session) try? await Task.sleep(nanoseconds: 10_000_000_000) overlay.close() self.overlayWindow = nil @@ -278,7 +285,7 @@ extension AppDelegate { self.showMainWindow() } await session.run() - let endMessage = self.computerUseEndMessage(for: session.state) + let endMessage = self.computerUseEndMessage(for: session) try? await Task.sleep(nanoseconds: 10_000_000_000) overlay.close() self.overlayWindow = nil diff --git a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift index bd4c5de92e3..54a102c5068 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift @@ -77,6 +77,8 @@ protocol ScreenRecording { @MainActor final class ScreenRecorder: NSObject, ScreenRecording { private(set) var isRecording = false + /// The file URL of the last recording attempt, available even after failure for salvage. + private(set) var lastRecordingFileURL: URL? private var stream: SCStream? private var assetWriter: AVAssetWriter? @@ -185,6 +187,7 @@ final class ScreenRecorder: NSObject, ScreenRecording { let fileName = "qa-recording-\(timestamp).mp4" let fileURL = recordingsDir.appendingPathComponent(fileName) recordingFileURL = fileURL + lastRecordingFileURL = fileURL // Set up AVAssetWriter let writer: AVAssetWriter @@ -297,8 +300,12 @@ final class ScreenRecorder: NSObject, ScreenRecording { await capturedWriter.finishWriting() if capturedWriter.status == .failed { - let errorMsg = capturedWriter.error?.localizedDescription ?? "Unknown error" - log.error("Asset writer failed: \(errorMsg)") + let writerError = capturedWriter.error + let nsError = writerError as? NSError + let errorMsg = writerError?.localizedDescription ?? "Unknown error" + let domain = nsError?.domain ?? "unknown" + let code = nsError?.code ?? -1 + log.error("Asset writer failed: \(errorMsg) (domain=\(domain), code=\(code))") throw ScreenRecorderError.assetWriterFailed(errorMsg) } diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 653062082ac..e2255657274 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -32,7 +32,7 @@ final class ComputerUseSession: ObservableObject { @Published var autoApproveTools = false { didSet { guard autoApproveTools != oldValue else { return } - log.info("Auto-approve tools toggled: \(autoApproveTools)") + log.info("Auto-approve tools toggled: \(self.autoApproveTools)") // Notify daemon so it can skip prompt creation proactively do { @@ -108,6 +108,7 @@ final class ComputerUseSession: ObservableObject { private var consecutiveUnchangedSteps = 0 private var currentStepNumber = 0 private var consecutiveFrontmostBlocks = 0 + private(set) var qaRecordingWarningMessage: String? /// Adaptive delay configuration private let adaptiveDelayEnabled: Bool @@ -168,6 +169,7 @@ final class ComputerUseSession: ObservableObject { isCancelled = false isPaused = false pendingToolPermissionPrompt = nil + qaRecordingWarningMessage = nil previousAXTreeText = nil previousElements = nil previousFlatElements = nil @@ -176,6 +178,12 @@ final class ComputerUseSession: ObservableObject { consecutiveFrontmostBlocks = 0 state = .running(step: 0, maxSteps: maxSteps, lastAction: "Starting...", reasoning: "") + // QA sessions auto-approve low/medium tools from the start. + // Set here (not in init) so the didSet fires and notifies the daemon. + if qaMode && !autoApproveTools { + autoApproveTools = true + } + log.info("Session starting — task: \(self.task, privacy: .public)") let screenSize = screenCapture.screenSize() @@ -217,6 +225,7 @@ final class ComputerUseSession: ObservableObject { log.info("QA mode: screen recording started for session \(self.id) (scope: \(self.captureScope))") } catch { log.error("QA mode: failed to start screen recording: \(error.localizedDescription)") + qaRecordingWarningMessage = "Unable to start recording. \(error.localizedDescription)" // Non-fatal — continue the session without recording } } @@ -1191,6 +1200,29 @@ final class ComputerUseSession: ObservableObject { log.info("QA recording finalized: \(result.fileURL.lastPathComponent) (\(result.sizeBytes) bytes, \(result.durationMs)ms)") } catch { log.error("QA mode: failed to stop screen recording: \(error.localizedDescription)") + if qaRecordingWarningMessage == nil { + qaRecordingWarningMessage = "Unable to finalize recording. \(error.localizedDescription)" + } + // Salvage: if the partial file exists on disk, track it for cleanup + if let salvageURL = (recorder as? ScreenRecorder)?.lastRecordingFileURL, + FileManager.default.fileExists(atPath: salvageURL.path) { + let attrs = try? FileManager.default.attributesOfItem(atPath: salvageURL.path) + let sizeBytes = (attrs?[.size] as? Int) ?? 0 + let expiresAtEpoch = Int(Date().addingTimeInterval(Double(retentionDays) * 24 * 3600).timeIntervalSince1970 * 1000) + log.info("QA mode: salvaging partial recording at \(salvageURL.lastPathComponent) (\(sizeBytes) bytes)") + recordingData = IPCCuSessionFinalizedRecording( + localPath: salvageURL.path, + mimeType: "video/mp4", + sizeBytes: sizeBytes, + durationMs: 0, + width: 0, + height: 0, + captureScope: captureScope, + includeAudio: includeAudio, + targetBundleId: nil, + expiresAt: expiresAtEpoch + ) + } } } diff --git a/clients/shared/DesignSystem/Core/Feedback/VToast.swift b/clients/shared/DesignSystem/Core/Feedback/VToast.swift index 30eed2af1c0..f5940852643 100644 --- a/clients/shared/DesignSystem/Core/Feedback/VToast.swift +++ b/clients/shared/DesignSystem/Core/Feedback/VToast.swift @@ -39,13 +39,16 @@ public struct VToast: View { } public var body: some View { - HStack(spacing: VSpacing.md) { + HStack(alignment: .top, spacing: VSpacing.md) { Image(systemName: iconName) .foregroundColor(iconColor) Text(message) .font(VFont.body) .foregroundColor(VColor.textPrimary) - .lineLimit(3) + .multilineTextAlignment(.leading) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .layoutPriority(1) Spacer(minLength: 0) diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index 1f669ff11ae..a14bced10fe 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -163,11 +163,7 @@ extension IPCCuSessionFinalized { } } -extension IPCCuSessionFinalizedRecording { - public init(localPath: String, mimeType: String, sizeBytes: Int, durationMs: Int, width: Int, height: Int, captureScope: String, includeAudio: Bool, targetBundleId: String?, expiresAt: Int) { - self.init(localPath: localPath, mimeType: mimeType, sizeBytes: sizeBytes, durationMs: durationMs, width: width, height: height, captureScope: captureScope, includeAudio: includeAudio, targetBundleId: targetBundleId, expiresAt: expiresAt) - } -} +// IPCCuSessionFinalizedRecording uses the generated memberwise init directly. /// Sent after each perceive step with AX tree, screenshot, and execution results. /// Backed by generated `IPCCuObservation`. From af41f7be5fad5df624ed62be8277994933bf5c4f Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:07:12 -0500 Subject: [PATCH 37/72] feat: IPC contract + config flag for recording enforcement (#7383) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../__snapshots__/ipc-snapshot.test.ts.snap | 38 +++++++++++++++++++ assistant/src/__tests__/ipc-snapshot.test.ts | 10 +++++ assistant/src/config/defaults.ts | 1 + assistant/src/config/schema.ts | 4 ++ assistant/src/daemon/computer-use-session.ts | 3 ++ assistant/src/daemon/handlers/computer-use.ts | 15 ++++++++ .../src/daemon/ipc-contract-inventory.json | 2 + assistant/src/daemon/ipc-contract.ts | 12 ++++++ .../IPC/Generated/IPCContractGenerated.swift | 24 +++++++++++- clients/shared/IPC/IPCMessages.swift | 14 ++++++- 10 files changed, 119 insertions(+), 4 deletions(-) diff --git a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap index e33f9cee353..e0e7255052d 100644 --- a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +++ b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap @@ -154,6 +154,14 @@ exports[`IPC message snapshots ClientMessage types cu_auto_approve_update serial } `; +exports[`IPC message snapshots ClientMessage types cu_recording_status serializes to expected JSON 1`] = ` +{ + "sessionId": "cu-sess-001", + "status": "started", + "type": "cu_recording_status", +} +`; + exports[`IPC message snapshots ClientMessage types cu_session_finalized serializes to expected JSON 1`] = ` { "sessionId": "cu-sess-001", @@ -926,6 +934,20 @@ exports[`IPC message snapshots ClientMessage types tool_names_list serializes to } `; +exports[`IPC message snapshots ClientMessage types dictation_request serializes to expected JSON 1`] = ` +{ + "context": { + "appName": "Example App", + "bundleIdentifier": "com.example.app", + "cursorInTextField": true, + "selectedText": "some selected text", + "windowTitle": "Main Window", + }, + "transcription": "Hello world", + "type": "dictation_request", +} +`; + exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = ` { "success": true, @@ -1341,6 +1363,14 @@ exports[`IPC message snapshots ServerMessage types task_routed serializes to exp } `; +exports[`IPC message snapshots ServerMessage types ride_shotgun_progress serializes to expected JSON 1`] = ` +{ + "message": "Analyzing screen content...", + "type": "ride_shotgun_progress", + "watchId": "watch-shotgun-001", +} +`; + exports[`IPC message snapshots ServerMessage types ride_shotgun_result serializes to expected JSON 1`] = ` { "observationCount": 5, @@ -2517,3 +2547,11 @@ exports[`IPC message snapshots ServerMessage types tool_names_list_response seri "type": "tool_names_list_response", } `; + +exports[`IPC message snapshots ServerMessage types dictation_response serializes to expected JSON 1`] = ` +{ + "mode": "dictation", + "text": "Hello world", + "type": "dictation_response", +} +`; diff --git a/assistant/src/__tests__/ipc-snapshot.test.ts b/assistant/src/__tests__/ipc-snapshot.test.ts index 51a7ffee363..2b42223dc6c 100644 --- a/assistant/src/__tests__/ipc-snapshot.test.ts +++ b/assistant/src/__tests__/ipc-snapshot.test.ts @@ -109,6 +109,11 @@ const clientMessages: Record = { sessionId: 'cu-sess-001', enabled: true, }, + cu_recording_status: { + type: 'cu_recording_status', + sessionId: 'cu-sess-001', + status: 'started', + }, cu_session_finalized: { type: 'cu_session_finalized', sessionId: 'cu-sess-001', @@ -847,6 +852,11 @@ const serverMessages: Record = { sessionId: 'sess-routed-001', interactionType: 'computer_use', }, + ride_shotgun_progress: { + type: 'ride_shotgun_progress', + watchId: 'watch-shotgun-001', + message: 'Analyzing screen content...', + }, ride_shotgun_result: { type: 'ride_shotgun_result', sessionId: 'sess-shotgun-001', diff --git a/assistant/src/config/defaults.ts b/assistant/src/config/defaults.ts index d694078bf4e..b1722c00081 100644 --- a/assistant/src/config/defaults.ts +++ b/assistant/src/config/defaults.ts @@ -257,6 +257,7 @@ export const DEFAULT_CONFIG: AssistantConfig = { cleanupIntervalMs: 6 * 60 * 60 * 1000, // 6 hours captureScope: 'display' as const, includeAudio: false, + enforceStartBeforeActions: false, }, sms: { enabled: false, diff --git a/assistant/src/config/schema.ts b/assistant/src/config/schema.ts index 5dc35119b9a..e02d940545a 100644 --- a/assistant/src/config/schema.ts +++ b/assistant/src/config/schema.ts @@ -1077,6 +1077,9 @@ export const QaRecordingConfigSchema = z.object({ includeAudio: z .boolean({ error: 'qaRecording.includeAudio must be a boolean' }) .default(false), + enforceStartBeforeActions: z + .boolean({ error: 'qaRecording.enforceStartBeforeActions must be a boolean' }) + .default(false), }); export const SmsConfigSchema = z.object({ @@ -1397,6 +1400,7 @@ export const AssistantConfigSchema = z.object({ cleanupIntervalMs: 6 * 60 * 60 * 1000, captureScope: 'display' as const, includeAudio: false, + enforceStartBeforeActions: false, }), sms: SmsConfigSchema.default({ enabled: false, diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index ae824a18147..8cdf6d42117 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -87,6 +87,9 @@ export class ComputerUseSession { /** When true, low/medium-risk tool prompts are auto-approved without client round-trip. */ autoApproveEnabled = false; + /** Tracks client-side recording lifecycle. Set by cu_recording_status handler. */ + recordingGateStatus: 'pending' | 'started' | 'failed' | 'stopped' = 'pending'; + // Tracks the agent loop promise so callers can await session completion private loopPromise: Promise | null = null; diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 838ff6b2713..877b52c90bb 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -8,6 +8,7 @@ import type { CuSessionCreate, CuSessionAbort, CuAutoApproveUpdate, + CuRecordingStatus, CuSessionFinalized, CuObservation, ServerMessage, @@ -381,10 +382,24 @@ export function handleCuAutoApproveUpdate( ); } +function handleCuRecordingStatus(msg: CuRecordingStatus, _socket: net.Socket, ctx: HandlerContext) { + const session = ctx.cuSessions.get(msg.sessionId); + if (!session) { + log.warn({ sessionId: msg.sessionId }, 'CU recording status for unknown session'); + return; + } + log.info( + { sessionId: msg.sessionId, status: msg.status, reason: msg.reason }, + 'CU recording status update', + ); + session.recordingGateStatus = msg.status; +} + export const computerUseHandlers = defineHandlers({ cu_session_create: handleCuSessionCreate, cu_session_abort: handleCuSessionAbort, cu_auto_approve_update: handleCuAutoApproveUpdate, + cu_recording_status: handleCuRecordingStatus, cu_session_finalized: handleCuSessionFinalized, cu_observation: handleCuObservation, }); diff --git a/assistant/src/daemon/ipc-contract-inventory.json b/assistant/src/daemon/ipc-contract-inventory.json index 2ea7f979709..58972c8f49d 100644 --- a/assistant/src/daemon/ipc-contract-inventory.json +++ b/assistant/src/daemon/ipc-contract-inventory.json @@ -22,6 +22,7 @@ "ConfirmationResponse", "CuAutoApproveUpdate", "CuObservation", + "CuRecordingStatus", "CuSessionAbort", "CuSessionCreate", "CuSessionFinalized", @@ -276,6 +277,7 @@ "confirmation_response", "cu_auto_approve_update", "cu_observation", + "cu_recording_status", "cu_session_abort", "cu_session_create", "cu_session_finalized", diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index c2f346a8804..0ae29addc50 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -185,6 +185,15 @@ export interface CuSessionCreate { targetAppName?: string; /** Optional target app bundle identifier for disambiguation. */ targetAppBundleId?: string; + /** When true, recording MUST start before any destructive action. */ + requiresRecording?: boolean; +} + +export interface CuRecordingStatus { + type: 'cu_recording_status'; + sessionId: string; + status: 'started' | 'failed' | 'stopped'; + reason?: string; } export interface CuSessionAbort { @@ -1085,6 +1094,7 @@ export type ClientMessage = | CuSessionCreate | CuSessionAbort | CuAutoApproveUpdate + | CuRecordingStatus | CuSessionFinalized | CuObservation | RideShotgunStart @@ -1593,6 +1603,8 @@ export interface TaskRouted { targetAppName?: string; /** Target app bundle ID for frontmost-app guard (from target-app-hints). */ targetAppBundleId?: string; + /** When true, recording MUST start before any destructive action. */ + requiresRecording?: boolean; } export interface RideShotgunProgress { diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 15516c4c411..858331c564a 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -915,6 +915,20 @@ public struct IPCCuObservation: Codable, Sendable { } } +public struct IPCCuRecordingStatus: Codable, Sendable { + public let type: String + public let sessionId: String + public let status: String + public let reason: String? + + public init(type: String, sessionId: String, status: String, reason: String?) { + self.type = type + self.sessionId = sessionId + self.status = status + self.reason = reason + } +} + public struct IPCCuSessionAbort: Codable, Sendable { public let type: String public let sessionId: String @@ -941,8 +955,10 @@ public struct IPCCuSessionCreate: Codable, Sendable { public let targetAppName: String? /// Optional target app bundle identifier for disambiguation. public let targetAppBundleId: String? + /// When true, recording MUST start before any destructive action. + public let requiresRecording: Bool? - public init(type: String, sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]?, interactionType: String?, reportToSessionId: String?, qaMode: Bool?, targetAppName: String?, targetAppBundleId: String?) { + public init(type: String, sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]?, interactionType: String?, reportToSessionId: String?, qaMode: Bool?, targetAppName: String?, targetAppBundleId: String?, requiresRecording: Bool?) { self.type = type self.sessionId = sessionId self.task = task @@ -954,6 +970,7 @@ public struct IPCCuSessionCreate: Codable, Sendable { self.qaMode = qaMode self.targetAppName = targetAppName self.targetAppBundleId = targetAppBundleId + self.requiresRecording = requiresRecording } } @@ -3512,8 +3529,10 @@ public struct IPCTaskRouted: Codable, Sendable { public let targetAppName: String? /// Target app bundle ID for frontmost-app guard (from target-app-hints). public let targetAppBundleId: String? + /// When true, recording MUST start before any destructive action. + public let requiresRecording: Bool? - public init(type: String, sessionId: String, interactionType: String, task: String?, escalatedFrom: String?, qaMode: Bool?, reportToSessionId: String?, retentionDays: Double?, captureScope: String?, includeAudio: Bool?, targetAppName: String?, targetAppBundleId: String?) { + public init(type: String, sessionId: String, interactionType: String, task: String?, escalatedFrom: String?, qaMode: Bool?, reportToSessionId: String?, retentionDays: Double?, captureScope: String?, includeAudio: Bool?, targetAppName: String?, targetAppBundleId: String?, requiresRecording: Bool?) { self.type = type self.sessionId = sessionId self.interactionType = interactionType @@ -3526,6 +3545,7 @@ public struct IPCTaskRouted: Codable, Sendable { self.includeAudio = includeAudio self.targetAppName = targetAppName self.targetAppBundleId = targetAppBundleId + self.requiresRecording = requiresRecording } } diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index a14bced10fe..f2a110fd254 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -148,8 +148,18 @@ extension IPCUserMessageAttachment { public typealias CuSessionCreateMessage = IPCCuSessionCreate extension IPCCuSessionCreate { - public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?, reportToSessionId: String? = nil, qaMode: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil) { - self.init(type: "cu_session_create", sessionId: sessionId, task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, interactionType: interactionType, reportToSessionId: reportToSessionId, qaMode: qaMode, targetAppName: targetAppName, targetAppBundleId: targetAppBundleId) + public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?, reportToSessionId: String? = nil, qaMode: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil, requiresRecording: Bool? = nil) { + self.init(type: "cu_session_create", sessionId: sessionId, task: task, screenWidth: screenWidth, screenHeight: screenHeight, attachments: attachments, interactionType: interactionType, reportToSessionId: reportToSessionId, qaMode: qaMode, targetAppName: targetAppName, targetAppBundleId: targetAppBundleId, requiresRecording: requiresRecording) + } +} + +/// Sent by the client to report recording lifecycle events. +/// Backed by generated `IPCCuRecordingStatus`. +public typealias CuRecordingStatusMessage = IPCCuRecordingStatus + +extension IPCCuRecordingStatus { + public init(sessionId: String, status: String, reason: String? = nil) { + self.init(type: "cu_recording_status", sessionId: sessionId, status: status, reason: reason) } } From 23d945ab55fd218b504b077f193c2c2c643942d0 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:09:29 -0500 Subject: [PATCH 38/72] feat: daemon action gate blocks destructive tools before recording starts (#7385) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/computer-use-session.ts | 108 ++++++++++++++++++ assistant/src/daemon/handlers/computer-use.ts | 4 + 2 files changed, 112 insertions(+) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 8cdf6d42117..d46b2dff61c 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -29,6 +29,7 @@ const log = getLogger('computer-use-session'); const MAX_STEPS = 50; const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes +const RECORDING_HANDSHAKE_TIMEOUT_MS = 8_000; // 8 seconds const MAX_HISTORY_ENTRIES = 10; const LOOP_DETECTION_WINDOW = 3; const CONSECUTIVE_UNCHANGED_WARNING_THRESHOLD = 2; @@ -42,6 +43,20 @@ const AX_TREE_PLACEHOLDER = ''; type SessionState = 'idle' | 'awaiting_observation' | 'inferring' | 'complete' | 'error'; +/** + * Tool names considered destructive — these MUST NOT run before recording has + * started when `requiresRecording` is true. + */ +const DESTRUCTIVE_TOOLS = new Set([ + 'computer_use_click', + 'computer_use_double_click', + 'computer_use_right_click', + 'computer_use_type_text', + 'computer_use_key', + 'computer_use_scroll', + 'computer_use_drag', +]); + interface ActionRecord { step: number; toolName: string; @@ -89,6 +104,12 @@ export class ComputerUseSession { /** Tracks client-side recording lifecycle. Set by cu_recording_status handler. */ recordingGateStatus: 'pending' | 'started' | 'failed' | 'stopped' = 'pending'; + /** Failure reason from the last cu_recording_status(failed) message. */ + recordingFailureReason?: string; + /** When true, destructive actions are blocked until recording has started. */ + private readonly requiresRecording: boolean; + /** Timer for the recording handshake timeout. */ + private recordingHandshakeTimer: ReturnType | null = null; // Tracks the agent loop promise so callers can await session completion private loopPromise: Promise | null = null; @@ -105,6 +126,7 @@ export class ComputerUseSession { preactivatedSkillIds?: string[], targetAppName?: string, targetAppBundleId?: string, + requiresRecording?: boolean, ) { this.sessionId = sessionId; this.task = task; @@ -117,6 +139,7 @@ export class ComputerUseSession { this.preactivatedSkillIds = preactivatedSkillIds ?? ['computer-use']; this.targetAppName = targetAppName; this.targetAppBundleId = targetAppBundleId; + this.requiresRecording = requiresRecording ?? false; } // --------------------------------------------------------------------------- @@ -167,6 +190,23 @@ export class ComputerUseSession { this.abort(); }, SESSION_TIMEOUT_MS); + // Recording handshake timeout: if requiresRecording is true and the + // recording gate is still pending after RECORDING_HANDSHAKE_TIMEOUT_MS, + // abort the session so it doesn't hang indefinitely. + if (this.requiresRecording && this.recordingGateStatus === 'pending') { + this.recordingHandshakeTimer = setTimeout(() => { + if (this.recordingGateStatus === 'pending') { + log.error( + { sessionId: this.sessionId, timeoutMs: RECORDING_HANDSHAKE_TIMEOUT_MS }, + 'Recording handshake timeout — recording never started', + ); + this.abortWithError( + 'Recording handshake timed out: recording did not start within 8 seconds. Session aborted to prevent unrecorded actions.', + ); + } + }, RECORDING_HANDSHAKE_TIMEOUT_MS); + } + const messages = this.buildMessages(obs, hadPreviousAXTree); this.loopPromise = this.runAgentLoop(messages).catch((err) => { // Catches errors from setup code (e.g. skill projection failures) that @@ -199,6 +239,10 @@ export class ComputerUseSession { clearTimeout(this.sessionTimer); this.sessionTimer = null; } + if (this.recordingHandshakeTimer) { + clearTimeout(this.recordingHandshakeTimer); + this.recordingHandshakeTimer = null; + } this.abortController?.abort(); // If waiting for an observation, resolve it as cancelled @@ -226,6 +270,44 @@ export class ComputerUseSession { this.notifyTerminal(); } + /** + * Abort the session with a specific error message. Used by the recording + * gate when a timeout or recording failure makes the session unrecoverable. + */ + private abortWithError(message: string): void { + if (this.state === 'complete' || this.state === 'error') return; + + log.error({ sessionId: this.sessionId }, message); + if (this.sessionTimer) { + clearTimeout(this.sessionTimer); + this.sessionTimer = null; + } + if (this.recordingHandshakeTimer) { + clearTimeout(this.recordingHandshakeTimer); + this.recordingHandshakeTimer = null; + } + this.abortController?.abort(); + + if (this.pendingObservation) { + this.pendingObservation.resolve({ content: message, isError: true }); + this.pendingObservation = null; + } + this.prompter?.dispose(); + for (const [, pending] of this.pendingSurfaceActions) { + pending.resolve({ content: message, isError: true }); + } + this.pendingSurfaceActions.clear(); + this.surfaceState.clear(); + + this.state = 'error'; + this.sendToClient({ + type: 'cu_error', + sessionId: this.sessionId, + message, + }); + this.notifyTerminal(); + } + isComplete(): boolean { return this.state === 'complete'; } @@ -587,6 +669,28 @@ export class ComputerUseSession { } } + // ── Recording gate: block destructive actions before recording ── + if (this.requiresRecording && this.recordingGateStatus !== 'started') { + if (this.recordingGateStatus === 'failed') { + const reason = this.recordingFailureReason ?? 'unknown'; + this.abortWithError(`Recording failed: ${reason}. Session cannot proceed without recording.`); + return { + content: `Recording failed: ${reason}. Session aborted.`, + isError: true, + }; + } + if (this.recordingGateStatus === 'pending' && DESTRUCTIVE_TOOLS.has(toolName)) { + log.warn( + { sessionId: this.sessionId, toolName, recordingGateStatus: this.recordingGateStatus }, + 'Blocked destructive action — recording has not started yet', + ); + return { + content: 'Recording has not started yet. Waiting for recording confirmation before executing destructive actions. Use computer_use_wait to wait, or computer_use_done/computer_use_respond if the task can be completed without interaction.', + isError: true, + }; + } + } + // ── Computer-use tool proxying ───────────────────────────────── const reasoning = typeof input.reasoning === 'string' ? input.reasoning : undefined; @@ -769,6 +873,10 @@ export class ComputerUseSession { clearTimeout(this.sessionTimer); this.sessionTimer = null; } + if (this.recordingHandshakeTimer) { + clearTimeout(this.recordingHandshakeTimer); + this.recordingHandshakeTimer = null; + } } } diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 877b52c90bb..1cb6865e798 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -98,6 +98,7 @@ export function handleCuSessionCreate( undefined, msg.targetAppName, msg.targetAppBundleId, + msg.requiresRecording, ); sessionRef.current = session; @@ -393,6 +394,9 @@ function handleCuRecordingStatus(msg: CuRecordingStatus, _socket: net.Socket, ct 'CU recording status update', ); session.recordingGateStatus = msg.status; + if (msg.status === 'failed' && msg.reason) { + session.recordingFailureReason = msg.reason; + } } export const computerUseHandlers = defineHandlers({ From 5f02309dd5a54c283bafd59f77cb3ab59b35fca4 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:09:43 -0500 Subject: [PATCH 39/72] feat: thread QA latch + deterministic requiresRecording routing (#7387) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/package-lock.json | 10997 ++++++++++++++++++++++ assistant/src/daemon/handlers/misc.ts | 29 +- assistant/src/daemon/handlers/shared.ts | 51 +- assistant/src/daemon/qa-intent.ts | 21 + 4 files changed, 11090 insertions(+), 8 deletions(-) create mode 100644 assistant/package-lock.json diff --git a/assistant/package-lock.json b/assistant/package-lock.json new file mode 100644 index 00000000000..361443a7c25 --- /dev/null +++ b/assistant/package-lock.json @@ -0,0 +1,10997 @@ +{ + "name": "@vellumai/assistant", + "version": "0.3.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vellumai/assistant", + "version": "0.3.4", + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.42", + "@anthropic-ai/sdk": "^0.39.0", + "@google/genai": "^1.40.0", + "@huggingface/transformers": "^3.8.1", + "@qdrant/js-client-rest": "^1.16.2", + "@sentry/node": "^10.38.0", + "agentmail": "^0.1.0", + "archiver": "^7.0.1", + "commander": "^13.1.0", + "croner": "^10.0.1", + "dotenv": "^17.3.1", + "drizzle-orm": "^0.38.4", + "ink": "^6.7.0", + "jszip": "^3.10.1", + "minimatch": "^10.1.2", + "openai": "^6.18.0", + "pino": "^9.6.0", + "pino-pretty": "^13.1.3", + "playwright": "^1.58.2", + "postgres": "^3.4.8", + "react": "^19.2.4", + "rrule": "^2.8.1", + "tldts": "^7.0.23", + "tree-sitter-bash": "0.25.1", + "uuid": "^11.1.0", + "web-tree-sitter": "0.26.5", + "zod": "^4.3.6" + }, + "bin": { + "vellum": "src/index.ts" + }, + "devDependencies": { + "@pydantic/logfire-node": "^0.13.0", + "@types/archiver": "^7.0.0", + "@types/bun": "^1.2.4", + "@types/node": "^25.2.2", + "@types/react": "^19.2.14", + "@types/uuid": "^10.0.0", + "drizzle-kit": "^0.30.4", + "eslint": "^10.0.0", + "fast-check": "^4.5.3", + "knip": "^5.83.1", + "quicktype-core": "^23.2.6", + "typescript": "^5.7.3", + "typescript-eslint": "^8.54.0", + "typescript-json-schema": "^0.67.1" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.5.tgz", + "integrity": "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.2.50", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.50.tgz", + "integrity": "sha512-zVQzJbicfTmvS6uarFQYYVYiYedKE0FgXmhiGC1oSLm6OkIbuuKM7XV4fXEFxPZHcWQc7ZYv6HA2/P5HOE7b2Q==", + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.34.2", + "@img/sharp-darwin-x64": "^0.34.2", + "@img/sharp-linux-arm": "^0.34.2", + "@img/sharp-linux-arm64": "^0.34.2", + "@img/sharp-linux-x64": "^0.34.2", + "@img/sharp-linuxmusl-arm64": "^0.34.2", + "@img/sharp-linuxmusl-x64": "^0.34.2", + "@img/sharp-win32-arm64": "^0.34.2", + "@img/sharp-win32-x64": "^0.34.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.8.2.tgz", + "integrity": "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==", + "license": "Apache-2.0" + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.3.1.tgz", + "integrity": "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==", + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.8.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.2", + "debug": "^4.3.1", + "minimatch": "^10.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@glideapps/ts-necessities": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@glideapps/ts-necessities/-/ts-necessities-2.2.3.tgz", + "integrity": "sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@google/genai": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.42.0.tgz", + "integrity": "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.5.tgz", + "integrity": "sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.210.0.tgz", + "integrity": "sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/auto-instrumentations-node": { + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.68.0.tgz", + "integrity": "sha512-WgLKBVG4hkaJYSfc/FbZsTr34gHVvMBd64Lw/u1bg3FLRGsd3Ys91672YRrj+7XPQXzwTew39sJRKMj03utRng==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/instrumentation-amqplib": "^0.57.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.62.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.65.0", + "@opentelemetry/instrumentation-bunyan": "^0.55.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.55.0", + "@opentelemetry/instrumentation-connect": "^0.53.0", + "@opentelemetry/instrumentation-cucumber": "^0.25.0", + "@opentelemetry/instrumentation-dataloader": "^0.27.0", + "@opentelemetry/instrumentation-dns": "^0.53.0", + "@opentelemetry/instrumentation-express": "^0.58.0", + "@opentelemetry/instrumentation-fastify": "^0.54.0", + "@opentelemetry/instrumentation-fs": "^0.29.0", + "@opentelemetry/instrumentation-generic-pool": "^0.53.0", + "@opentelemetry/instrumentation-graphql": "^0.57.0", + "@opentelemetry/instrumentation-grpc": "^0.210.0", + "@opentelemetry/instrumentation-hapi": "^0.56.0", + "@opentelemetry/instrumentation-http": "^0.210.0", + "@opentelemetry/instrumentation-ioredis": "^0.58.0", + "@opentelemetry/instrumentation-kafkajs": "^0.19.0", + "@opentelemetry/instrumentation-knex": "^0.54.0", + "@opentelemetry/instrumentation-koa": "^0.58.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.54.0", + "@opentelemetry/instrumentation-memcached": "^0.53.0", + "@opentelemetry/instrumentation-mongodb": "^0.63.0", + "@opentelemetry/instrumentation-mongoose": "^0.56.0", + "@opentelemetry/instrumentation-mysql": "^0.56.0", + "@opentelemetry/instrumentation-mysql2": "^0.56.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.56.0", + "@opentelemetry/instrumentation-net": "^0.54.0", + "@opentelemetry/instrumentation-openai": "^0.8.0", + "@opentelemetry/instrumentation-oracledb": "^0.35.0", + "@opentelemetry/instrumentation-pg": "^0.62.0", + "@opentelemetry/instrumentation-pino": "^0.56.0", + "@opentelemetry/instrumentation-redis": "^0.58.0", + "@opentelemetry/instrumentation-restify": "^0.55.0", + "@opentelemetry/instrumentation-router": "^0.54.0", + "@opentelemetry/instrumentation-runtime-node": "^0.23.0", + "@opentelemetry/instrumentation-socket.io": "^0.56.0", + "@opentelemetry/instrumentation-tedious": "^0.29.0", + "@opentelemetry/instrumentation-undici": "^0.20.0", + "@opentelemetry/instrumentation-winston": "^0.54.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.33.0", + "@opentelemetry/resource-detector-aws": "^2.10.0", + "@opentelemetry/resource-detector-azure": "^0.18.0", + "@opentelemetry/resource-detector-container": "^0.8.1", + "@opentelemetry/resource-detector-gcp": "^0.45.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-node": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^2.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.210.0.tgz", + "integrity": "sha512-tM0ROS/hZM72kB55cSjDcghVcUXBJdGkGzpkhD7M1B/gpcvZPSGfjFgKN3dgmxNgF76NxtbUwv3ik0wS+Kz52g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/configuration/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz", + "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", + "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.210.0.tgz", + "integrity": "sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/sdk-logs": "0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.210.0.tgz", + "integrity": "sha512-Q8/SEQtgrErbVVRg9M9iaG8m5wdPNdU0UOF7U43sAhwfmPG92ZOk/aenKhg0DXSNJHhkCDNCgS1kSoErAB3z0A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.210.0", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/sdk-logs": "0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.210.0.tgz", + "integrity": "sha512-Y/yPc+gDhsWB7AsNzQWxblw4ULbvhCycMaQ2aAn+HSAVbgbMiZa0SbclPVHSnpnNzKSLVavFjweAr0pQA1KKLg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.210.0", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-logs": "0.210.0", + "@opentelemetry/sdk-trace-base": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.210.0.tgz", + "integrity": "sha512-pWZ/Tjrqev9rdkqe8F6A9FGddLZrjl6iRAU5LBvvRL6I3PSgG8z1xM0cESAy1jzAF4wGohnAh8rB7hHzpUOYEA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.210.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-metrics": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz", + "integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.210.0.tgz", + "integrity": "sha512-JpLThG8Hh8A/Jzdzw9i4Ftu+EzvLaX/LouN+mOOHmadL0iror0Qsi3QWzucXeiUsDDsiYgjfKyi09e6sltytgA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-metrics": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz", + "integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.210.0.tgz", + "integrity": "sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.210.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-metrics": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz", + "integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.210.0.tgz", + "integrity": "sha512-8i+7d70Hho6pcheTtbqIuS+bo+AIX/oNUTMwIEZoehUE4ZdbGmeVaE+hJS2LAErFeFaU71w164lAgYyMUEQ8zw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-metrics": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz", + "integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.210.0.tgz", + "integrity": "sha512-1GPLOyxIfUX24WM8Oea+vx9d9TlewposUnsQXTjusxVMQ/dWvt5JIDJyTsfNDS412XRUOORgF97PwsfDY5QKGA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.210.0.tgz", + "integrity": "sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.210.0.tgz", + "integrity": "sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.4.0.tgz", + "integrity": "sha512-qpiXY0TUEFjBBp9b1na9LfuVQw6W8LH+te7uv+CC+0Up78ZDtZZwOjK2M7CL7Nspnw+yS4JdgEA7oxsBu0Ctsg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.210.0.tgz", + "integrity": "sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.210.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.57.0.tgz", + "integrity": "sha512-hgHnbcopDXju7164mwZu7+6mLT/+O+6MsyedekrXL+HQAYenMqeG7cmUOE0vI6s/9nW08EGHXpD+Q9GhLU1smA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-lambda": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.62.0.tgz", + "integrity": "sha512-EbDyOwdN4ndn0JJq7qacZLSCxrm72lj/2j98/MjakCuTG15nBJ/R4OkdzOmmoPbtvnrgGzzBZBiaDYoviJEAFA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "^8.10.155" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-aws-sdk": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.65.0.tgz", + "integrity": "sha512-nrKIhTlBxFr/wvjk2vZ6eCcyc41eOQVTMR+ux4FM0gNvK+DgggE+RnkycGATP5lJKjltn+wrYNP2E2tmxCtF1A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.55.0.tgz", + "integrity": "sha512-/iBimXTUbxsEHpLafOLiYhinS8NQo2pRhkJAeviepfSegJkBnR9ACu5YoiJN/CsKM6HpW8UTpecZXHfu+rM0CQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "^0.210.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@types/bunyan": "1.8.11" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.55.0.tgz", + "integrity": "sha512-o7ud8Fcg6HFKooWKSWk8ouMVGy3UBv6jg5PVQp/teng/tw7tbLNlZNGW7W6IzUcVfpToBfkh78iQPAKrzryLfg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.53.0.tgz", + "integrity": "sha512-SoFqipWLUEYVIxvz0VYX9uWLJhatJG4cqXpRe1iophLofuEtqFUn8YaEezjz2eJK74eTUQ0f0dJVOq7yMXsJGQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-cucumber": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.25.0.tgz", + "integrity": "sha512-0Rmrt2DJjinfeuThg1E6Rfr7vGOnrrQxezh3QV1YtVpp8pY+365CsBLjTJmQ2J6zsSWbbZJ0l9Fhtjw13a78Wg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.27.0.tgz", + "integrity": "sha512-8e7n8edfTN28nJDpR/H59iW3RbW1fvpt0xatGTfSbL8JS4FLizfjPxO7JLbyWh9D3DSXxrTnvOvXpt6V5pnxJg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dns": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.53.0.tgz", + "integrity": "sha512-/m4KxS7rWkQpTLJW77cyt0pNzdcgjm2at4XD0nLGhHJz2G3x8GXQ6QOLRc3kPYt1WHJvzQ2UgzjKDz7f83PUXQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.58.0.tgz", + "integrity": "sha512-UuGst6/1XPcswrIm5vmhuUwK/9qx9+fmNB+4xNk3lfpgQlnQxahy20xmlo3I+LIyA5ZA3CR2CDXslxAMqwminA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fastify": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.54.0.tgz", + "integrity": "sha512-1sIJmA7wuvtNSrFbQek9rl2SNXvQ2JNuitDixL0kWiqL/UkJYkeSSegxmuEg52AAcO5Aa2OtJc0L2Syz/XROYw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.29.0.tgz", + "integrity": "sha512-JXPygU1RbrHNc5kD+626v3baV5KamB4RD4I9m9nUTd/HyfLZQSA3Z2z3VOebB3ChJhRDERmQjLiWvwJMHecKPg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.53.0.tgz", + "integrity": "sha512-h49axGXGlvWzyQ4exPyd0qG9EUa+JP+hYklFg6V+Gm4ZC2Zam1QeJno/TQ8+qrLvsVvaFnBjTdS53hALpR3h3Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.57.0.tgz", + "integrity": "sha512-wjtSavcp9MsGcnA1hj8ArgsL3EkHIiTLGMwqVohs5pSnMGeao0t2mgAuMiv78KdoR3kO3DUjks8xPO5Q6uJekg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-grpc": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.210.0.tgz", + "integrity": "sha512-gwXtFydErdqM6Vq/DMNst1Vb6aRPdZHIA155rgD06QGeqyg+0RQxtW3SCmCzGMwrlMTrqPBIfG/v757Zi4skLA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "0.210.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.56.0.tgz", + "integrity": "sha512-HgLxgO0G8V9y/6yW2pS3Fv5M3hz9WtWUAdbuszQDZ8vXDQSd1sI9FYHLdZW+td/8xCLApm8Li4QIeCkRSpHVTg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.210.0.tgz", + "integrity": "sha512-dICO+0D0VBnrDOmDXOvpmaP0gvai6hNhJ5y6+HFutV0UoXc7pMgJlJY3O7AzT725cW/jP38ylmfHhQa7M0Nhww==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/instrumentation": "0.210.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.58.0.tgz", + "integrity": "sha512-2tEJFeoM465A0FwPB0+gNvdM/xPBRIqNtC4mW+mBKy+ZKF9CWa7rEqv87OODGrigkEDpkH8Bs1FKZYbuHKCQNQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.19.0.tgz", + "integrity": "sha512-PMJePP4PVv+NSvWFuKADEVemsbNK8tnloHnrHOiRXMmBnyqcyOTmJyPy6eeJ0au90QyiGB2rzD8smmu2Y0CC7A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.54.0.tgz", + "integrity": "sha512-XYXKVUH+0/Ur29jMPnyxZj32MrZkWSXHhCteTkt/HzynKnvIASmaAJ6moMOgBSRoLuDJFqPew68AreRylIzhhg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.58.0.tgz", + "integrity": "sha512-602W6hEFi3j2QrQQBKWuBUSlHyrwSCc1IXpmItC991i9+xJOsS4n4mEktEk/7N6pavBX35J9OVkhPDXjbFk/1A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.54.0.tgz", + "integrity": "sha512-LPji0Qwpye5e1TNAUkHt7oij2Lrtpn2DRTUr4CU69VzJA13aoa2uzP3NutnFoLDUjmuS6vi/lv08A2wo9CfyTA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-memcached": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.53.0.tgz", + "integrity": "sha512-ni6B1n5wdY3XsbfL74Ix5yKQsXRerrgqmhK595ICgkxlU6JDwxoaCmoGmLCKDS/Nr0p3XhIfPVvjOPCfK73nUw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/memcached": "^2.2.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.63.0.tgz", + "integrity": "sha512-EvJb3aLiq1QedAZO4vqXTG0VJmKUpGU37r11thLPuL5HNa08sUS9DbF69RB8YoXVby2pXkFPMnbG0Pky0JMlKA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.56.0.tgz", + "integrity": "sha512-1xBjUpDSJFZS4qYc4XXef0pzV38iHyKymY4sKQ3xPv7dGdka4We1PsuEg6Z8K21f1d2Yg5eU0OXXRSPVmowKfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.56.0.tgz", + "integrity": "sha512-osdGMB3vc4bm1Kos04zfVmYAKoKVbKiF/Ti5/R0upDEOsCnrnUm9xvLeaKKbbE2WgJoaFz3VS8c99wx31efytQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.56.0.tgz", + "integrity": "sha512-rW0hIpoaCFf55j0F1oqw6+Xv9IQeqJGtw9MudT3LCuhqld9S3DF0UEj8o3CZuPhcYqD+HAivZQdrsO5XMWyFqw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.56.0.tgz", + "integrity": "sha512-2wKd6+/nKyZVTkElTHRZAAEQ7moGqGmTIXlZvfAeV/dNA+6zbbl85JBcyeUFIYt+I42Naq5RgKtUY8fK6/GE1g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-net": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.54.0.tgz", + "integrity": "sha512-Vpfw1AXCGbIdL+xrvXWIx/l1dG3H7kixvFLuOY8QWFsw5+nThAUKwVCVau4VIMzWnY9TC1Oa86NIEc0ILga4CQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-openai": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.8.0.tgz", + "integrity": "sha512-iX/AZLXrbRfwhOv7Cn5vNHR+o3tvtjAi44r2tS0eL1+lW75IvkN4SK6NDJjqWBEv2sIM1TsqydOMfUf1fV1sxw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "^0.210.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.35.0.tgz", + "integrity": "sha512-V9DG842WFbcjtb9EpSRcA49vySKAzM7csVk490wOrxsjZ0QCliUQ8GH06cnYiSQi/OOYS2NMPuRKQNhrDWB8Jw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@types/oracledb": "6.5.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.62.0.tgz", + "integrity": "sha512-/ZSMRCyFRMjQVx7Wf+BIAOMEdN/XWBbAGTNLKfQgGYs1GlmdiIFkUy8Z8XGkToMpKrgZju0drlTQpqt4Ul7R6w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.56.0.tgz", + "integrity": "sha512-S2YMh+xfLanyhhGSzZwFxO8iUFJoSdBO/qlndSbkrmlydFJrOrA3nyZQclM0E1i3IN+uXjMJkGRN7B5R7am+yg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "^0.210.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.58.0.tgz", + "integrity": "sha512-tOGxw+6HZ5LDpMP05zYKtTw5HPqf3PXYHaOuN+pkv6uIgrZ+gTT75ELkd49eXBpjg3t36p8bYpsLgYcpIPqWqA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-restify": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.55.0.tgz", + "integrity": "sha512-vpAHMoiLGdKz44zFzop283JLksuuO9EM7ap03cj0UgbxcaEjjLGkIv2qAEcICtYi/1LBRGHk1fXlmUg3Mu36dQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-router": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.54.0.tgz", + "integrity": "sha512-N+PATM9akOUnfQTYnc0eDb6uRxpCkZMLFGxWgDIc0SclTKYqhu8WQjHPWK82YnUQ3ghgt+3BckPZihiOctRhdA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.23.0.tgz", + "integrity": "sha512-CWq1xxuVUkOqOAzTcEiNvgT/rxKpoegC4z92eNqeYS5e71OTU8DeFZ9bNrtbb1YtbYCeY4ROBPyhMWJl27br3w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-socket.io": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.56.0.tgz", + "integrity": "sha512-YueOOdNsMI9vUv+T8VMDv5TLEoBLF/UFfgr/InZ+H9+WRBhG9iGaRFJ8cvjx1EOz/wP5nFdcBgffMyphjhWYQA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.29.0.tgz", + "integrity": "sha512-Jtnayb074lk7DQL25pOOpjvg4zjJMFjFWOLlKzTF5i1KxMR4+GlR/DSYgwDRfc0a4sfPXzdb/yYw7jRSX/LdFg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.20.0.tgz", + "integrity": "sha512-VGBQ89Bza1pKtV12Lxgv3uMrJ1vNcf1cDV6LAXp2wa6hnl6+IN6lbEmPn6WNWpguZTZaFEvugyZgN8FJuTjLEA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.54.0.tgz", + "integrity": "sha512-RH8HVPXrSYgozn+D3SANXcDxt3Xcd8If85JWmGRTns45Hu8YfXA3DEVonA8YfVg4zvvEJbGg+RFbCddAX/6LaA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "^0.210.0", + "@opentelemetry/instrumentation": "^0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.210.0.tgz", + "integrity": "sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-transformer": "0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.210.0.tgz", + "integrity": "sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/otlp-exporter-base": "0.210.0", + "@opentelemetry/otlp-transformer": "0.210.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.210.0.tgz", + "integrity": "sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.210.0", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-logs": "0.210.0", + "@opentelemetry/sdk-metrics": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0", + "protobufjs": "8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz", + "integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.4.0.tgz", + "integrity": "sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.4.0.tgz", + "integrity": "sha512-t6muBL/3AMD++1EMF658C/KIpj3gfmTmftX3mEQql4KIxNGFvacCmmTtrQt9IZAJmQRfjQRCkv+vsGbQugeJIw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", + "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.33.2.tgz", + "integrity": "sha512-EaS54zwYmOg9Ttc79juaktpCBYqyh2IquXl534sLls+c1/pc8LZfWPMqytFt+iBvSPQ6ajraUnvi6cun4AhSjQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-aws": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.12.0.tgz", + "integrity": "sha512-VelueKblsnQEiBVqEYcvM9VEb+B8zN6nftltdO9HAD7qi/OlicP4z/UGJ9EeW2m++WabdMoj0G3QVL8YV0P9tw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.18.0.tgz", + "integrity": "sha512-7byUo/Gimruh23vA8H2q4/cWxGe7YOTBjIKpoPjt/9yGQ2PUF3s6k2SQrMxaonTwqVmQgW29DU3SHLfd0kHjhg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.3.tgz", + "integrity": "sha512-5J0JP2cy655rBKM9Doz26ffO3rG+Xqm7OXeNXkckzmc3JmL6Bj3dPBKugPYsfemhEIqtf7INH9UmPQqTMuWoHg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resource-detector-gcp": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.45.0.tgz", + "integrity": "sha512-u1AshqWqiiSblTix+8zzR9hcUWiHMNW/4WUnOecZ3FgNMkJJ57rkZvQKxaK5mfetP0s1OfAbiq09krQ1riO/Rw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "gcp-metadata": "^6.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", + "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.210.0.tgz", + "integrity": "sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.210.0", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.1.tgz", + "integrity": "sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.210.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.210.0.tgz", + "integrity": "sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/api-logs": "0.210.0", + "@opentelemetry/configuration": "0.210.0", + "@opentelemetry/context-async-hooks": "2.4.0", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.210.0", + "@opentelemetry/exporter-logs-otlp-http": "0.210.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.210.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.210.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.210.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.210.0", + "@opentelemetry/exporter-prometheus": "0.210.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.210.0", + "@opentelemetry/exporter-trace-otlp-http": "0.210.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.210.0", + "@opentelemetry/exporter-zipkin": "2.4.0", + "@opentelemetry/instrumentation": "0.210.0", + "@opentelemetry/propagator-b3": "2.4.0", + "@opentelemetry/propagator-jaeger": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/sdk-logs": "0.210.0", + "@opentelemetry/sdk-metrics": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0", + "@opentelemetry/sdk-trace-node": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/context-async-hooks": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.4.0.tgz", + "integrity": "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz", + "integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz", + "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.1", + "@opentelemetry/resources": "2.5.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.4.0.tgz", + "integrity": "sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/context-async-hooks": "2.4.0", + "@opentelemetry/core": "2.4.0", + "@opentelemetry/sdk-trace-base": "2.4.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.4.0.tgz", + "integrity": "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", + "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/resources": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", + "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", + "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/core": "2.4.0", + "@opentelemetry/resources": "2.4.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", + "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.18.0.tgz", + "integrity": "sha512-EhwJNzbfLwQQIeyak3n08EB3UHknMnjy1dFyL98r3xlorje2uzHOT2vkB5nB1zqtTtzT31uSot3oGZFfODbGUg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.18.0.tgz", + "integrity": "sha512-esOPsT9S9B6vEMMp1qR9Yz5UepQXljoWRJYoyp7GV/4SYQOSTpN0+V2fTruxbMmzqLK+fjCEU2x3SVhc96LQLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.18.0.tgz", + "integrity": "sha512-iJknScn8fRLRhGR6VHG31bzOoyLihSDmsJHRjHwRUL0yF1MkLlvzmZ+liKl9MGl+WZkZHaOFT5T1jNlLSWTowQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.18.0.tgz", + "integrity": "sha512-3rMweF2GQLzkaUoWgFKy1fRtk0dpj4JDqucoZLJN9IZG+TC+RZg7QMwG5WKMvmEjzdYmOTw1L1XqZDVXF2ksaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.18.0.tgz", + "integrity": "sha512-TfXsFby4QvpGwmUP66+X+XXQsycddZe9ZUUu/vHhq2XGI1EkparCSzjpYW1Nz5fFncbI5oLymQLln/qR+qxyOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.18.0.tgz", + "integrity": "sha512-WolOILquy9DJsHcfFMHeA5EjTCI9A7JoERFJru4UI2zKZcnfNPo5GApzYwiloscEp/s+fALPmyRntswUns0qHg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.18.0.tgz", + "integrity": "sha512-r+5nHJyPdiBqOGTYAFyuq5RtuAQbm4y69GYWNG/uup9Cqr7RG9Ak0YZgGEbkQsc+XBs00ougu/D1+w3UAYIWHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.18.0.tgz", + "integrity": "sha512-bUzg6QxljqMLLwsxYajAQEHW1LYRLdKOg/aykt14PSqUUOmfnOJjPdSLTiHIZCluVzPCQxv1LjoyRcoTAXfQaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.18.0.tgz", + "integrity": "sha512-l43GVwls5+YR8WXOIez5x7Pp/MfhdkMOZOOjFUSWC/9qMnSLX1kd95j9oxDrkWdD321JdHTyd4eau5KQPxZM9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.18.0.tgz", + "integrity": "sha512-ayj7TweYWi/azxWmRpUZGz41kKNvfkXam20UrFhaQDrSNGNqefQRODxhJn0iv6jt4qChh7TUxDIoavR6ftRsjw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.18.0.tgz", + "integrity": "sha512-2Jz7jpq6BBNlBBup3usZB6sZWEZOBbjWn++/bKC2lpAT+sTEwdTonnf3rNcb+XY7+v53jYB9pM8LEKVXZfr8BA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.18.0.tgz", + "integrity": "sha512-omw8/ISOc6ubR247iEMma4/JRfbY2I+nGJC59oKBhCIEZoyqEg/NmDSBc4ToMH+AsZDucqQUDOCku3k7pBiEag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.18.0.tgz", + "integrity": "sha512-uFipBXaS+honSL5r5G/rlvVrkffUjpKwD3S/aIiwp64bylK3+RztgV+mM1blk+OT5gBRG864auhH6jCfrOo3ZA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.18.0.tgz", + "integrity": "sha512-bY4uMIoKRv8Ine3UiKLFPWRZ+fPCDamTHZFf5pNOjlfmTJIANtJo0mzWDUdFZLYhVgQdegrDL9etZbTMR8qieg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.18.0.tgz", + "integrity": "sha512-40IicL/aitfNOWur06x7Do41WcqFJ9VUNAciFjZCXzF6wR2i6uVsi6N19ecqgSRoLYFCAoRYi9F50QteIxCwKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.18.0.tgz", + "integrity": "sha512-DJIzYjUnSJtz4Trs/J9TnzivtPcUKn9AeL3YjHlM5+RvK27ZL9xISs3gg2VAo2nWU7ThuadC1jSYkWaZyONMwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.18.0.tgz", + "integrity": "sha512-57+R8Ioqc8g9k80WovoupOoyIOfLEceHTizkUcwOXspXLhiZ67ScM7Q8OuvhDoRRSZzH6yI0qML3WZwMFR3s7g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.18.0.tgz", + "integrity": "sha512-t9Oa4BPptJqVlHTT1cV1frs+LY/vjsKhHI6ltj2EwoGM1TykJ0WW43UlQaU4SC8N+oTY8JRbAywVMNkfqjSu9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.18.0.tgz", + "integrity": "sha512-4maf/f6ea5IEtIXqGwSw38srRtVHTre9iKShG4gjzat7c3Iq6B1OppXMj8gNmTuM4n8Xh1hQM9z2hBELccJr1g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.18.0.tgz", + "integrity": "sha512-EhW8Su3AEACSw5HfzKMmyCtV0oArNrVViPdeOfvVYL9TrkL+/4c8fWHFTBtxUMUyCjhSG5xYNdwty1D/TAgL0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/instrumentation": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.2.0.tgz", + "integrity": "sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.207.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz", + "integrity": "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { + "version": "0.207.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz", + "integrity": "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.207.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@pydantic/logfire-node": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@pydantic/logfire-node/-/logfire-node-0.13.0.tgz", + "integrity": "sha512-gIU2+06v6tmZChI7yFIEv8+LFmd5OUOw7R5IPgF6s1FCpm6G0QzCxuxiFMXAr8DN+RG3K1sG1tXXqLfq7jQRwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "logfire": "0.13.0", + "picocolors": "^1.1.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.68.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.210.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.210.0", + "@opentelemetry/instrumentation": "^0.210.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-metrics": "^2.0.0", + "@opentelemetry/sdk-node": "^0.210.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@qdrant/js-client-rest": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.17.0.tgz", + "integrity": "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==", + "license": "Apache-2.0", + "dependencies": { + "@qdrant/openapi-typescript-fetch": "1.2.6", + "undici": "^6.23.0" + }, + "engines": { + "node": ">=18.17.0", + "pnpm": ">=8" + }, + "peerDependencies": { + "typescript": ">=4.7" + } + }, + "node_modules/@qdrant/openapi-typescript-fetch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.6.tgz", + "integrity": "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.39.0.tgz", + "integrity": "sha512-xCLip2mBwCdRrvXHtVEULX0NffUTYZZBhEUGht0WFL+GNdNQ7gmBOGOczhZlrf2hgFFtDO0fs1xiP9bqq5orEQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.39.0.tgz", + "integrity": "sha512-dx66DtU/xkCTPEDsjU+mYSIEbzu06pzKNQcDA2wvx7wvwsUciZ5yA32Ce/o6p2uHHgy0/joJX9rP5J/BIijaOA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.5.0", + "@opentelemetry/core": "^2.5.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/instrumentation-amqplib": "0.58.0", + "@opentelemetry/instrumentation-connect": "0.54.0", + "@opentelemetry/instrumentation-dataloader": "0.28.0", + "@opentelemetry/instrumentation-express": "0.59.0", + "@opentelemetry/instrumentation-fs": "0.30.0", + "@opentelemetry/instrumentation-generic-pool": "0.54.0", + "@opentelemetry/instrumentation-graphql": "0.58.0", + "@opentelemetry/instrumentation-hapi": "0.57.0", + "@opentelemetry/instrumentation-http": "0.211.0", + "@opentelemetry/instrumentation-ioredis": "0.59.0", + "@opentelemetry/instrumentation-kafkajs": "0.20.0", + "@opentelemetry/instrumentation-knex": "0.55.0", + "@opentelemetry/instrumentation-koa": "0.59.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.55.0", + "@opentelemetry/instrumentation-mongodb": "0.64.0", + "@opentelemetry/instrumentation-mongoose": "0.57.0", + "@opentelemetry/instrumentation-mysql": "0.57.0", + "@opentelemetry/instrumentation-mysql2": "0.57.0", + "@opentelemetry/instrumentation-pg": "0.63.0", + "@opentelemetry/instrumentation-redis": "0.59.0", + "@opentelemetry/instrumentation-tedious": "0.30.0", + "@opentelemetry/instrumentation-undici": "0.21.0", + "@opentelemetry/resources": "^2.5.0", + "@opentelemetry/sdk-trace-base": "^2.5.0", + "@opentelemetry/semantic-conventions": "^1.39.0", + "@prisma/instrumentation": "7.2.0", + "@sentry/core": "10.39.0", + "@sentry/node-core": "10.39.0", + "@sentry/opentelemetry": "10.39.0", + "import-in-the-middle": "^2.0.6", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.39.0.tgz", + "integrity": "sha512-xdeBG00TmtAcGvXnZNbqOCvnZ5kY3s5aT/L8wUQ0w0TT2KmrC9XL/7UHUfJ45TLbjl10kZOtaMQXgUjpwSJW+g==", + "license": "MIT", + "dependencies": { + "@apm-js-collab/tracing-hooks": "^0.3.1", + "@sentry/core": "10.39.0", + "@sentry/opentelemetry": "10.39.0", + "import-in-the-middle": "^2.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/resources": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/semantic-conventions": "^1.39.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/context-async-hooks": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/resources": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "@opentelemetry/semantic-conventions": { + "optional": true + } + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/api-logs": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.58.0.tgz", + "integrity": "sha512-fjpQtH18J6GxzUZ+cwNhWUpb71u+DzT7rFkg5pLssDGaEber91Y2WNGdpVpwGivfEluMlNMZumzjEqfg8DeKXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.54.0.tgz", + "integrity": "sha512-43RmbhUhqt3uuPnc16cX6NsxEASEtn8z/cYV8Zpt6EP4p2h9s4FNuJ4Q9BbEQ2C0YlCCB/2crO1ruVz/hWt8fA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.28.0.tgz", + "integrity": "sha512-ExXGBp0sUj8yhm6Znhf9jmuOaGDsYfDES3gswZnKr4MCqoBWQdEFn6EoDdt5u+RdbxQER+t43FoUihEfTSqsjA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-express": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.59.0.tgz", + "integrity": "sha512-pMKV/qnHiW/Q6pmbKkxt0eIhuNEtvJ7sUAyee192HErlr+a1Jx+FZ3WjfmzhQL1geewyGEiPGkmjjAgNY8TgDA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.30.0.tgz", + "integrity": "sha512-n3Cf8YhG7reaj5dncGlRIU7iT40bxPOjsBEA5Bc1a1g6e9Qvb+JFJ7SEiMlPbUw4PBmxE3h40ltE8LZ3zVt6OA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.54.0.tgz", + "integrity": "sha512-8dXMBzzmEdXfH/wjuRvcJnUFeWzZHUnExkmFJ2uPfa31wmpyBCMxO59yr8f/OXXgSogNgi/uPo9KW9H7LMIZ+g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.58.0.tgz", + "integrity": "sha512-+yWVVY7fxOs3j2RixCbvue8vUuJ1inHxN2q1sduqDB0Wnkr4vOzVKRYl/Zy7B31/dcPS72D9lo/kltdOTBM3bQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.57.0.tgz", + "integrity": "sha512-Os4THbvls8cTQTVA8ApLfZZztuuqGEeqog0XUnyRW7QVF0d/vOVBEcBCk1pazPFmllXGEdNbbat8e2fYIWdFbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-http": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.211.0.tgz", + "integrity": "sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/instrumentation": "0.211.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.59.0.tgz", + "integrity": "sha512-875UxzBHWkW+P4Y45SoFM2AR8f8TzBMD8eO7QXGCyFSCUMP5s9vtt/BS8b/r2kqLyaRPK6mLbdnZznK3XzQWvw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.20.0.tgz", + "integrity": "sha512-yJXOuWZROzj7WmYCUiyT27tIfqBrVtl1/TwVbQyWPz7rL0r1Lu7kWjD0PiVeTCIL6CrIZ7M2s8eBxsTAOxbNvw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.55.0.tgz", + "integrity": "sha512-FtTL5DUx5Ka/8VK6P1VwnlUXPa3nrb7REvm5ddLUIeXXq4tb9pKd+/ThB1xM/IjefkRSN3z8a5t7epYw1JLBJQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.59.0.tgz", + "integrity": "sha512-K9o2skADV20Skdu5tG2bogPKiSpXh4KxfLjz6FuqIVvDJNibwSdu5UvyyBzRVp1rQMV6UmoIk6d3PyPtJbaGSg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.55.0.tgz", + "integrity": "sha512-FDBfT7yDGcspN0Cxbu/k8A0Pp1Jhv/m7BMTzXGpcb8ENl3tDj/51U65R5lWzUH15GaZA15HQ5A5wtafklxYj7g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.64.0.tgz", + "integrity": "sha512-pFlCJjweTqVp7B220mCvCld1c1eYKZfQt1p3bxSbcReypKLJTwat+wbL2YZoX9jPi5X2O8tTKFEOahO5ehQGsA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.57.0.tgz", + "integrity": "sha512-MthiekrU/BAJc5JZoZeJmo0OTX6ycJMiP6sMOSRTkvz5BrPMYDqaJos0OgsLPL/HpcgHP7eo5pduETuLguOqcg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.57.0.tgz", + "integrity": "sha512-HFS/+FcZ6Q7piM7Il7CzQ4VHhJvGMJWjx7EgCkP5AnTntSN5rb5Xi3TkYJHBKeR27A0QqPlGaCITi93fUDs++Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.57.0.tgz", + "integrity": "sha512-nHSrYAwF7+aV1E1V9yOOP9TchOodb6fjn4gFvdrdQXiRE7cMuffyLLbCZlZd4wsspBzVwOXX8mpURdRserAhNA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.63.0.tgz", + "integrity": "sha512-dKm/ODNN3GgIQVlbD6ZPxwRc3kleLf95hrRWXM+l8wYo+vSeXtEpQPT53afEf6VFWDVzJK55VGn8KMLtSve/cg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.59.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.59.0.tgz", + "integrity": "sha512-JKv1KDDYA2chJ1PC3pLP+Q9ISMQk6h5ey+99mB57/ARk0vQPGZTTEb4h4/JlcEpy7AYT8HIGv7X6l+br03Neeg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.30.0.tgz", + "integrity": "sha512-bZy9Q8jFdycKQ2pAsyuHYUHNmCxCOGdG6eg1Mn75RvQDccq832sU5OWOBnc12EFUELI6icJkhR7+EQKMBam2GA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@sentry/node/node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.21.0.tgz", + "integrity": "sha512-gok0LPUOTz2FQ1YJMZzaHcOzDFyT64XJ8M9rNkugk923/p6lDGms/cRW1cqgqp6N6qcd6K6YdVHwPEhnx9BWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@sentry/node/node_modules/minimatch": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.39.0.tgz", + "integrity": "sha512-eU8t/pyxjy7xYt6PNCVxT+8SJw5E3pnupdcUNN4ClqG4O5lX4QCDLtId48ki7i30VqrLtR7vmCHMSvqXXdvXPA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", + "@opentelemetry/semantic-conventions": "^1.39.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readdir-glob": "*" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.160", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.160.tgz", + "integrity": "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/bun": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.9.tgz", + "integrity": "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.9" + } + }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/memcached": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", + "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/oracledb": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz", + "integrity": "sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", + "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/agentmail": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/agentmail/-/agentmail-0.1.19.tgz", + "integrity": "sha512-L0au0GnyH24Ob7l0QrkkdwJWL25Aa26f6YnuQlpvwtQMVOTu6IiMftJ8LnVVwQtKwBSZ6cWLS+9SdCvgw+LWGg==", + "dependencies": { + "ws": "^8.16.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-or-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-3.0.0.tgz", + "integrity": "sha512-iczIdVJzGEYhP5DqQxYM9Hh7Ztpqqi+CXZpSmX8ALFs9ecXkQIeqRyM6TfxEfMVpwhl3dSuDvxdzzo9sUOIVBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bun-types": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.9.tgz", + "integrity": "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/collection-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collection-utils/-/collection-utils-1.0.1.tgz", + "integrity": "sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/croner": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/croner/-/croner-10.0.1.tgz", + "integrity": "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==", + "funding": [ + { + "type": "other", + "url": "https://paypal.me/hexagonpp" + }, + { + "type": "github", + "url": "https://github.com/sponsors/hexagon" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-kit": { + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", + "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0", + "gel": "^2.0.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.38.4.tgz", + "integrity": "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.1", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", + "integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^7.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gel": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gel/-/gel-2.2.0.tgz", + "integrity": "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@petamoriken/float16": "^3.8.7", + "debug": "^4.3.4", + "env-paths": "^3.0.0", + "semver": "^7.6.2", + "shell-quote": "^1.8.1", + "which": "^4.0.0" + }, + "bin": { + "gel": "dist/cli.mjs" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/gel/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/gel/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gtoken/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gtoken/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ink": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.8.0.tgz", + "integrity": "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.2.4", + "ansi-escapes": "^7.3.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.6.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^5.1.1", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.39.10", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "scheduler": "^0.27.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^8.0.0", + "stack-utils": "^2.0.6", + "string-width": "^8.1.1", + "terminal-size": "^4.0.1", + "type-fest": "^5.4.1", + "widest-line": "^6.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/react": ">=19.0.0", + "react": ">=19.0.0", + "react-devtools-core": ">=6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true, + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/knip": { + "version": "5.85.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.85.0.tgz", + "integrity": "sha512-V2kyON+DZiYdNNdY6GALseiNCwX7dYdpz9Pv85AUn69Gk0UKCts+glOKWfe5KmaMByRjM9q17Mzj/KinTVOyxg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "@nodelib/fs.walk": "^1.2.3", + "fast-glob": "^3.3.3", + "formatly": "^0.3.0", + "jiti": "^2.6.0", + "js-yaml": "^4.1.1", + "minimist": "^1.2.8", + "oxc-resolver": "^11.15.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.1", + "smol-toml": "^1.5.2", + "strip-json-comments": "5.0.3", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": ">=18.18.0" + }, + "peerDependencies": { + "@types/node": ">=18", + "typescript": ">=5.0.4 <7" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/logfire": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/logfire/-/logfire-0.13.0.tgz", + "integrity": "sha512-A2Q1jQXRrL2el5JFLlqaF8UNs2RxmDGqPM3iECH1wAT53KiPXJj9EsuCPVsqj3Xpj2gSEvMrQ6eVwZ4evSWSGQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "license": "MIT" + }, + "node_modules/openai": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.23.0.tgz", + "integrity": "sha512-w6NJofZ12lUQLm5W8RJcqq0HhGE4gZuqVFrBA1q40qx0Uyn/kcrSbOY542C2WHtyTZLz9ucNr4WUO46m8r43YQ==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.18.0.tgz", + "integrity": "sha512-Fv/b05AfhpYoCDvsog6tgsDm2yIwIeJafpMFLncNwKHRYu+Y1xQu5Q/rgUn7xBfuhNgjtPO7C0jCf7p2fLDj1g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.18.0", + "@oxc-resolver/binding-android-arm64": "11.18.0", + "@oxc-resolver/binding-darwin-arm64": "11.18.0", + "@oxc-resolver/binding-darwin-x64": "11.18.0", + "@oxc-resolver/binding-freebsd-x64": "11.18.0", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.18.0", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.18.0", + "@oxc-resolver/binding-linux-arm64-gnu": "11.18.0", + "@oxc-resolver/binding-linux-arm64-musl": "11.18.0", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.18.0", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.18.0", + "@oxc-resolver/binding-linux-riscv64-musl": "11.18.0", + "@oxc-resolver/binding-linux-s390x-gnu": "11.18.0", + "@oxc-resolver/binding-linux-x64-gnu": "11.18.0", + "@oxc-resolver/binding-linux-x64-musl": "11.18.0", + "@oxc-resolver/binding-openharmony-arm64": "11.18.0", + "@oxc-resolver/binding-wasm32-wasi": "11.18.0", + "@oxc-resolver/binding-win32-arm64-msvc": "11.18.0", + "@oxc-resolver/binding-win32-ia32-msvc": "11.18.0", + "@oxc-resolver/binding-win32-x64-msvc": "11.18.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz", + "integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/quicktype-core": { + "version": "23.2.6", + "resolved": "https://registry.npmjs.org/quicktype-core/-/quicktype-core-23.2.6.tgz", + "integrity": "sha512-asfeSv7BKBNVb9WiYhFRBvBZHcRutPRBwJMxW0pefluK4kkKu4lv0IvZBwFKvw2XygLcL1Rl90zxWDHYgkwCmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@glideapps/ts-necessities": "2.2.3", + "browser-or-node": "^3.0.0", + "collection-utils": "^1.0.1", + "cross-fetch": "^4.0.0", + "is-url": "^1.2.4", + "js-base64": "^3.7.7", + "lodash": "^4.17.21", + "pako": "^1.0.6", + "pluralize": "^8.0.0", + "readable-stream": "4.5.2", + "unicode-properties": "^1.4.1", + "urijs": "^1.19.1", + "wordwrap": "^1.0.0", + "yaml": "^2.4.1" + } + }, + "node_modules/quicktype-core/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz", + "integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rrule": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz", + "integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smol-toml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", + "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tar": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-sitter-bash": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", + "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-json-schema": { + "version": "0.67.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.67.1.tgz", + "integrity": "sha512-vKTZB/RoYTIBdVP7E7vrgHMCssBuhja91wQy498QIVhvfRimaOgjc98uwAXmZ7mbLUytJmOSbF11wPz+ByQeXg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/node": "^18.11.9", + "glob": "^7.1.7", + "path-equal": "^1.2.5", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~5.5.0", + "vm2": "^3.10.0", + "yargs": "^17.1.1" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/typescript-json-schema/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript-json-schema/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/typescript-json-schema/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-json-schema/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-json-schema/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vm2": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.10.5.tgz", + "integrity": "sha512-3P/2QDccVFBcujfCOeP8vVNuGfuBJHEuvGR8eMmI10p/iwLL2UwF5PDaNaoOS2pRGQEDmJRyeEcc8kmm2Z59RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "acorn-walk": "^8.3.4" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/web-tree-sitter": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.5.tgz", + "integrity": "sha512-u9sl+q21VSKX2T8dhpQw8bMGGqNfwaIyuoYE3kdOQGVDrOqrmcS9GmaQoCS602iaFnuokn3WCHW374c7GAnuaQ==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", + "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", + "license": "MIT", + "dependencies": { + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index 40baeee4d84..ab44c951d31 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -18,9 +18,9 @@ import type { IpcBlobProbe, CuSessionCreate, } from '../ipc-protocol.js'; -import { log, wireEscalationHandler, renderHistoryContent, defineHandlers, type HandlerContext } from './shared.js'; +import { log, wireEscalationHandler, renderHistoryContent, defineHandlers, setQaLatch, clearQaLatch, isQaLatchActive, type HandlerContext } from './shared.js'; import { handleCuSessionCreate } from './computer-use.js'; -import { detectQaIntent, shouldRouteQaToComputerUse } from '../qa-intent.js'; +import { detectQaIntent, detectQaOptOut, shouldRouteQaToComputerUse } from '../qa-intent.js'; import { resolveComputerUseTargetAppHint } from '../target-app-hints.js'; // ─── Task submit handler ──────────────────────────────────────────────────── @@ -69,25 +69,42 @@ export async function handleTaskSubmit( // Slash candidates always route to text_qa — bypass classifier const slashCandidate = parseSlashCandidate(msg.task); const isQa = detectQaIntent(msg.task); + const isOptOut = detectQaOptOut(msg.task); + const qaLatchActive = isQaLatchActive(msg.conversationId); const forceQaComputerUse = shouldRouteQaToComputerUse(msg.task); const interactionType = slashCandidate.kind === 'candidate' ? 'text_qa' as const : forceQaComputerUse ? 'computer_use' as const : await classifyInteraction(msg.task, msg.source); + + // Update QA latch: set on QA intent, clear on explicit opt-out + if (isQa && msg.conversationId) { + setQaLatch(msg.conversationId); + } + if (isOptOut && msg.conversationId) { + clearQaLatch(msg.conversationId); + } + + const config = getConfig(); + // Determine whether recording is required: (QA intent or active latch) + config flag + const requiresRecording = (isQa || (qaLatchActive && !isOptOut)) && config.qaRecording.enforceStartBeforeActions; + rlog.info({ interactionType, slashBypass: slashCandidate.kind === 'candidate', taskLength: msg.task.length, isQa, forceQaComputerUse, + qaLatchActive, + requiresRecording, }, 'Task classified'); if (interactionType === 'computer_use') { // Create CU session (reuse handleCuSessionCreate logic) const sessionId = uuid(); const targetApp = resolveComputerUseTargetAppHint(msg.task); - const config = getConfig(); + const effectiveQa = isQa || (qaLatchActive && !isOptOut); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId, @@ -97,7 +114,8 @@ export async function handleTaskSubmit( attachments: msg.attachments, interactionType: 'computer_use', ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(isQa ? { qaMode: true, reportToSessionId: msg.conversationId } : {}), + ...(effectiveQa ? { qaMode: true, reportToSessionId: msg.conversationId } : {}), + ...(requiresRecording ? { requiresRecording: true } : {}), }; handleCuSessionCreate(cuMsg, socket, ctx); @@ -106,13 +124,14 @@ export async function handleTaskSubmit( sessionId, interactionType: 'computer_use', ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(isQa ? { + ...(effectiveQa ? { qaMode: true, reportToSessionId: msg.conversationId, retentionDays: config.qaRecording.defaultRetentionDays, captureScope: config.qaRecording.captureScope, includeAudio: config.qaRecording.includeAudio, } : {}), + ...(requiresRecording ? { requiresRecording: true } : {}), }); } else { // Create text QA session and immediately start processing diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index 78c8d757ed1..3b3656bec06 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -9,11 +9,39 @@ import type { ClientMessage, CuSessionCreate, ServerMessage, SessionTransportMet import type { SecretPromptResult } from '../../permissions/secret-prompter.js'; import { getConfig } from '../../config/loader.js'; import type { DebouncerMap } from '../../util/debounce.js'; -import { detectQaIntent } from '../qa-intent.js'; +import { detectQaIntent, detectQaOptOut } from '../qa-intent.js'; import { resolveComputerUseTargetAppHint } from '../target-app-hints.js'; const log = getLogger('handlers'); +/** + * Per-conversation QA latch. When a QA intent is detected in a conversation, + * the latch is set so subsequent CU turns in that thread default to + * requiresRecording=true. Cleared when the user explicitly opts out. + */ +export const qaLatchByConversation = new Map(); + +/** + * Set the QA latch for a conversation (thread). + */ +export function setQaLatch(conversationId: string): void { + qaLatchByConversation.set(conversationId, true); +} + +/** + * Clear the QA latch for a conversation (thread). + */ +export function clearQaLatch(conversationId: string): void { + qaLatchByConversation.delete(conversationId); +} + +/** + * Check whether the QA latch is active for a conversation. + */ +export function isQaLatchActive(conversationId: string | undefined): boolean { + return conversationId != null && (qaLatchByConversation.get(conversationId) === true); +} + export { log }; /** Debounce window for suppressing file-watcher config reloads after programmatic saves. */ @@ -238,8 +266,23 @@ export function wireEscalationHandler( const cuSessionId = uuid(); const isQa = detectQaIntent(task); + const qaLatchActive = isQaLatchActive(sourceSessionId); const targetApp = resolveComputerUseTargetAppHint(task); const config = getConfig(); + + // Determine whether recording is required: QA intent or latch + config flag + const requiresRecording = (isQa || qaLatchActive) && config.qaRecording.enforceStartBeforeActions; + + // Set the QA latch for this conversation when QA intent is detected + if (isQa) { + setQaLatch(sourceSessionId); + } + + // Check for explicit opt-out + if (detectQaOptOut(task)) { + clearQaLatch(sourceSessionId); + } + const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId: cuSessionId, @@ -249,7 +292,8 @@ export function wireEscalationHandler( interactionType: 'computer_use', reportToSessionId: sourceSessionId, ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(isQa ? { qaMode: true } : {}), + ...(isQa || qaLatchActive ? { qaMode: true } : {}), + ...(requiresRecording ? { requiresRecording: true } : {}), }; handleCuSessionCreate(cuMsg, currentSocket, ctx); @@ -261,12 +305,13 @@ export function wireEscalationHandler( escalatedFrom: sourceSessionId, reportToSessionId: sourceSessionId, ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(isQa ? { + ...(isQa || qaLatchActive ? { qaMode: true, retentionDays: config.qaRecording.defaultRetentionDays, captureScope: config.qaRecording.captureScope, includeAudio: config.qaRecording.includeAudio, } : {}), + ...(requiresRecording ? { requiresRecording: true } : {}), }); return true; diff --git a/assistant/src/daemon/qa-intent.ts b/assistant/src/daemon/qa-intent.ts index 9066b1a0a71..46aa3847346 100644 --- a/assistant/src/daemon/qa-intent.ts +++ b/assistant/src/daemon/qa-intent.ts @@ -1,3 +1,24 @@ +/** + * Detect whether the user is explicitly opting out of QA/recording mode. + * Used to clear the thread-level QA latch. + */ +export function detectQaOptOut(text: string): boolean { + const lower = text.toLowerCase().trim(); + const optOutPatterns = [ + /\bstop\s+qa\s+mode\b/, + /\bno\s+recording\b/, + /\bdisable\s+recording\b/, + /\bstop\s+testing\b/, + /\bstop\s+recording\b/, + /\bturn\s+off\s+qa\b/, + /\bturn\s+off\s+recording\b/, + /\bend\s+qa\s+mode\b/, + /\bexit\s+qa\s+mode\b/, + /\bquit\s+qa\s+mode\b/, + ]; + return optOutPatterns.some(p => p.test(lower)); +} + /** * Detect whether a user's task text indicates a QA/test workflow. * Uses keyword/pattern matching for v1 — can be upgraded to semantic detection later. From 63544735f78c3c01fba177679f7445009fad4e4e Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:15:38 -0500 Subject: [PATCH 40/72] feat: fail-closed recorder handshake with CuRecordingStatus IPC (#7388) - Add requiresRecording field to IPCTaskRouted and IPCCuSessionCreate - Add IPCCuRecordingStatus IPC message type for recording status notifications - When requiresRecording is true, abort session if recording fails to start - Send CuRecordingStatus "started"/"failed"/"stopped" messages to daemon - On finalize, fail the session if recording was required but no artifact exists - Pass requiresRecording from TaskRouted through to ComputerUseSession - Best-effort recording behavior preserved when requiresRecording is false/nil Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../App/AppDelegate+Sessions.swift | 2 + .../ComputerUse/Session.swift | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 590059ec3cb..452659d83ef 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -115,6 +115,7 @@ extension AppDelegate { retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, captureScope: routed.captureScope ?? "display", includeAudio: routed.includeAudio ?? false, + requiresRecording: routed.requiresRecording ?? false, targetAppName: routed.targetAppName, targetAppBundleId: routed.targetAppBundleId ) @@ -266,6 +267,7 @@ extension AppDelegate { retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, captureScope: routed.captureScope ?? "display", includeAudio: routed.includeAudio ?? false, + requiresRecording: routed.requiresRecording ?? false, targetAppName: routed.targetAppName, targetAppBundleId: routed.targetAppBundleId ) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index e2255657274..24a2a9f8c97 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -80,6 +80,8 @@ final class ComputerUseSession: ObservableObject { let captureScope: String /// Whether to include audio in QA recording (from daemon config, default false). let includeAudio: Bool + /// When true, recording MUST start before any action; failure aborts the session. + let requiresRecording: Bool /// Target app name for frontmost-app guard — nil means no constraint. let targetAppName: String? @@ -136,6 +138,7 @@ final class ComputerUseSession: ObservableObject { retentionDays: Int = 7, captureScope: String = "display", includeAudio: Bool = false, + requiresRecording: Bool = false, targetAppName: String? = nil, targetAppBundleId: String? = nil ) { @@ -158,6 +161,7 @@ final class ComputerUseSession: ObservableObject { self.retentionDays = retentionDays self.captureScope = captureScope self.includeAudio = includeAudio + self.requiresRecording = requiresRecording self.targetAppName = targetAppName self.targetAppBundleId = targetAppBundleId self.verifier = ActionVerifier(maxSteps: maxSteps) @@ -223,10 +227,24 @@ final class ComputerUseSession: ObservableObject { } try await recorder.startRecording(windowID: windowID, displayID: displayID, includeAudio: self.includeAudio) log.info("QA mode: screen recording started for session \(self.id) (scope: \(self.captureScope))") + // Notify daemon that recording started successfully + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "started")) } catch { log.error("QA mode: failed to start screen recording: \(error.localizedDescription)") + // Notify daemon of recording failure + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: error.localizedDescription)) + if requiresRecording { + // Fail-closed: recording is mandatory — abort the session + state = .failed(reason: "Recording required but failed to start: \(error.localizedDescription)") + qaRecordingWarningMessage = "Recording required but failed: \(error.localizedDescription)" + logger.finishSession(result: "failed: required recording could not start") + cancelSafetyNetTask?.cancel() + cancelSafetyNetTask = nil + await finalizeQARecording() + return + } + // Non-fatal for best-effort recording — continue the session without recording qaRecordingWarningMessage = "Unable to start recording. \(error.localizedDescription)" - // Non-fatal — continue the session without recording } } @@ -258,7 +276,8 @@ final class ComputerUseSession: ObservableObject { reportToSessionId: reportToSessionId, qaMode: qaMode ? true : nil, targetAppName: targetAppName, - targetAppBundleId: targetAppBundleId + targetAppBundleId: targetAppBundleId, + requiresRecording: requiresRecording ? true : nil )) } catch { log.error("Failed to send session create message: \(error)") @@ -1198,8 +1217,11 @@ final class ComputerUseSession: ObservableObject { expiresAt: expiresAtEpoch ) log.info("QA recording finalized: \(result.fileURL.lastPathComponent) (\(result.sizeBytes) bytes, \(result.durationMs)ms)") + // Notify daemon that recording stopped successfully + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "stopped")) } catch { log.error("QA mode: failed to stop screen recording: \(error.localizedDescription)") + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: error.localizedDescription)) if qaRecordingWarningMessage == nil { qaRecordingWarningMessage = "Unable to finalize recording. \(error.localizedDescription)" } @@ -1226,6 +1248,27 @@ final class ComputerUseSession: ObservableObject { } } + // If recording was required but no artifact was produced, mark as failure + if requiresRecording && recordingData == nil && status != "failed" { + log.error("QA mode: recording was required but no recording artifact exists") + // Override status to failed — the session cannot be considered successful without a recording + let failedStatus = "failed" + let failedSummary = "Recording required but no recording artifact produced" + do { + try daemonClient.send(CuSessionFinalizedMessage( + sessionId: id, + status: failedStatus, + summary: failedSummary, + stepCount: stepCount, + recording: nil + )) + log.info("QA mode: sent cu_session_finalized for session \(self.id) (status: \(failedStatus))") + } catch { + log.error("QA mode: failed to send cu_session_finalized: \(error.localizedDescription)") + } + return + } + // Send cu_session_finalized to the daemon do { try daemonClient.send(CuSessionFinalizedMessage( From 6bddc7140026bf5c8907bb8a0d14b740b681e675 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:19:20 -0500 Subject: [PATCH 41/72] feat: recording status indicator + expandable error text in CU overlay (#7396) Add a recording status badge to the session overlay when qaMode is true: - Green dot + "Recording" when recording is active - Red dot + error message (scrollable) when recording failed - Yellow dot + "Recording required - waiting..." when waiting for required recording - Spinner + "Recording starting..." during initialization Make the failed-state error text scrollable (up to 120px) with text selection enabled, preventing silent truncation of long error messages. Changes: - Session.swift: Add @Published isRecordingActive and make qaRecordingWarningMessage @Published so the overlay can observe recording state transitions - SessionOverlayView.swift: Add recordingStatusView component, include recording warning text in overlay width calculation, wrap failed-state text in ScrollView Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../ComputerUse/Session.swift | 8 +- .../Features/Session/SessionOverlayView.swift | 79 +++++++++++++++++-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 24a2a9f8c97..b8efdaa229d 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -110,7 +110,9 @@ final class ComputerUseSession: ObservableObject { private var consecutiveUnchangedSteps = 0 private var currentStepNumber = 0 private var consecutiveFrontmostBlocks = 0 - private(set) var qaRecordingWarningMessage: String? + @Published private(set) var qaRecordingWarningMessage: String? + /// Whether screen recording is currently active (for UI indicator). + @Published private(set) var isRecordingActive: Bool = false /// Adaptive delay configuration private let adaptiveDelayEnabled: Bool @@ -174,6 +176,7 @@ final class ComputerUseSession: ObservableObject { isPaused = false pendingToolPermissionPrompt = nil qaRecordingWarningMessage = nil + isRecordingActive = false previousAXTreeText = nil previousElements = nil previousFlatElements = nil @@ -227,6 +230,7 @@ final class ComputerUseSession: ObservableObject { } try await recorder.startRecording(windowID: windowID, displayID: displayID, includeAudio: self.includeAudio) log.info("QA mode: screen recording started for session \(self.id) (scope: \(self.captureScope))") + isRecordingActive = true // Notify daemon that recording started successfully try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "started")) } catch { @@ -1216,10 +1220,12 @@ final class ComputerUseSession: ObservableObject { targetBundleId: result.targetBundleId, expiresAt: expiresAtEpoch ) + isRecordingActive = false log.info("QA recording finalized: \(result.fileURL.lastPathComponent) (\(result.sizeBytes) bytes, \(result.durationMs)ms)") // Notify daemon that recording stopped successfully try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "stopped")) } catch { + isRecordingActive = false log.error("QA mode: failed to stop screen recording: \(error.localizedDescription)") try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: error.localizedDescription)) if qaRecordingWarningMessage == nil { diff --git a/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift b/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift index 98d3cd37413..bf55e84913f 100644 --- a/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift +++ b/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift @@ -21,6 +21,9 @@ struct SessionOverlayView: View { candidates.append(prompt.toolName) candidates.append(prompt.summary) } + if let warning = session.qaRecordingWarningMessage { + candidates.append(warning) + } switch session.state { case .running(_, _, let lastAction, let reasoning): candidates.append(lastAction) @@ -58,6 +61,9 @@ struct SessionOverlayView: View { Divider() + // Recording status indicator (QA mode only) + recordingStatusView + if let prompt = session.pendingToolPermissionPrompt { toolPermissionPromptView(prompt) Divider() @@ -222,13 +228,19 @@ struct SessionOverlayView: View { } case .failed(let reason): - HStack(spacing: 6) { + HStack(alignment: .top, spacing: 6) { Image(systemName: "xmark.circle.fill") .foregroundStyle(.red) - Text(reason) - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) + .padding(.top, 1) + ScrollView { + Text(reason) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + } + .frame(maxHeight: 120) } case .cancelled: @@ -238,6 +250,63 @@ struct SessionOverlayView: View { } } + @ViewBuilder + private var recordingStatusView: some View { + if session.qaMode { + if let warning = session.qaRecordingWarningMessage { + HStack(alignment: .top, spacing: VSpacing.sm) { + Circle() + .fill(VColor.error) + .frame(width: 8, height: 8) + .padding(.top, 3) + ScrollView { + Text(warning) + .font(VFont.caption) + .foregroundColor(VColor.error) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 60) + } + .padding(.horizontal, VSpacing.xs) + } else if session.isRecordingActive { + HStack(spacing: VSpacing.sm) { + Circle() + .fill(VColor.success) + .frame(width: 8, height: 8) + Text("Recording") + .font(VFont.caption) + .foregroundColor(VColor.success) + } + .padding(.horizontal, VSpacing.xs) + } else if session.requiresRecording { + HStack(spacing: VSpacing.sm) { + Circle() + .fill(VColor.warning) + .frame(width: 8, height: 8) + Text("Recording required \u{2014} waiting...") + .font(VFont.caption) + .foregroundColor(VColor.warning) + } + .padding(.horizontal, VSpacing.xs) + } else { + switch session.state { + case .idle, .running(step: 0, _, _, _): + HStack(spacing: VSpacing.sm) { + ProgressView() + .controlSize(.mini) + Text("Recording starting...") + .font(VFont.caption) + .foregroundStyle(.secondary) + } + .padding(.horizontal, VSpacing.xs) + default: + EmptyView() + } + } + } + } + @ViewBuilder private var controlButtons: some View { switch session.state { From 5f480ee66fb85cc443b818a1e4be9403df4e494c Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:37:03 -0500 Subject: [PATCH 42/72] fix: canonicalize app name variants in cross-app detection (#7409) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- assistant/src/daemon/computer-use-session.ts | 23 ++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index d46b2dff61c..7853489b3f3 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -333,6 +333,22 @@ export class ComputerUseSession { return value.toLowerCase().replace(/[^a-z0-9]/g, ''); } + /** + * Maps variant / long-form app names to a canonical short form so that + * e.g. "google chrome" and "chrome" are counted as the same app in + * {@link taskExplicitlyRequestsCrossApp}. Keys must be lowercase. + */ + private static readonly APP_CANONICAL_MAP: ReadonlyMap = new Map([ + ['google chrome', 'chrome'], + ['microsoft teams', 'teams'], + ['apple notes', 'notes'], + ['apple music', 'music'], + ['vellum assistant', 'vellum'], + ['visual studio code', 'vscode'], + ['iterm2', 'iterm'], + ['gmail', 'mail'], + ]); + /** * Well-known app names used to detect cross-app intent in task text. * Only needs to cover apps commonly referenced in cross-app workflows; @@ -379,8 +395,11 @@ export class ComputerUseSession { const escaped = appName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (new RegExp(`\\b${escaped}\\b`).test(t)) { // Normalize to a canonical form so e.g. "google chrome" and "chrome" - // are counted as the same app. - const canonical = ComputerUseSession.normalizeAppLabel(appName); + // are counted as the same app. Check the canonical map first so + // variant names like "google chrome" collapse to the same key as + // "chrome" instead of normalizing to distinct strings. + const canonical = ComputerUseSession.APP_CANONICAL_MAP.get(appName) + ?? ComputerUseSession.normalizeAppLabel(appName); mentionedApps.add(canonical); } // Early exit once we confirm at least two distinct apps. From d52b122ab6483a618d649d28b8adf9941356fd2d Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:37:49 -0500 Subject: [PATCH 43/72] fix: avoid double prompt on fresh screen recording request (#7411) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../App/PermissionManager.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/clients/macos/vellum-assistant/App/PermissionManager.swift b/clients/macos/vellum-assistant/App/PermissionManager.swift index eb0d938d164..a70919e5393 100644 --- a/clients/macos/vellum-assistant/App/PermissionManager.swift +++ b/clients/macos/vellum-assistant/App/PermissionManager.swift @@ -22,24 +22,29 @@ enum PermissionManager { static func requestScreenRecordingAccess() { let hasRequestedBefore = UserDefaults.standard.bool(forKey: hasRequestedScreenRecordingFlag) + let preflightBeforeRequest = CGPreflightScreenCaptureAccess() // CGRequestScreenCaptureAccess() only shows the native OS prompt on // its very first invocation per app install; subsequent calls are // no-ops. The API is non-blocking, so CGPreflightScreenCaptureAccess() // returns false immediately — before the user has a chance to respond - // to the prompt. On the first call we therefore trust the native prompt - // and skip the System Settings fallback to avoid showing both at once. + // to the prompt. CGRequestScreenCaptureAccess() if !hasRequestedBefore { UserDefaults.standard.set(true, forKey: hasRequestedScreenRecordingFlag) - // For legacy installs that denied screen recording before this flag - // existed: CGRequestScreenCaptureAccess() was a no-op, so check if - // permission is still denied and fall back to System Settings. - if CGPreflightScreenCaptureAccess() == false { - openScreenRecordingSettings() - } - } else if !CGPreflightScreenCaptureAccess() { + // On first request we cannot distinguish between a fresh install + // (where the native prompt just appeared) and a legacy denied + // install (where CGRequestScreenCaptureAccess() was a no-op). + // In both cases CGPreflightScreenCaptureAccess() returns false. + // To avoid opening System Settings alongside the native prompt + // (double-prompt), we only open Settings when we know the user + // had already been prompted before — i.e. hasRequestedBefore was + // true. Legacy denied users will get the Settings fallback on + // their next interaction. + } else if !preflightBeforeRequest { + // Permission was already denied before this request — the native + // prompt won't appear, so open System Settings as a fallback. openScreenRecordingSettings() } } From 6475fb39df0b50d57e9a3247822f705063cefd61 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:38:02 -0500 Subject: [PATCH 44/72] fix: frontmost guard sends abort + resets counter on non-guard blocks (#7412) Co-authored-by: Vellum Assistant Co-authored-by: Claude --- .../vellum-assistant/ComputerUse/Session.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index b8efdaa229d..0ce3c505b59 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -445,6 +445,8 @@ final class ComputerUseSession: ObservableObject { currentStepNumber = action.stepNumber guard let resolved = await resolveCoordinatesIfNeeded(for: agentAction, stepNumber: action.stepNumber) else { + // Coordinate resolution failures are not frontmost-guard blocks — reset the consecutive streak + consecutiveFrontmostBlocks = 0 return } agentAction = resolved @@ -470,6 +472,7 @@ final class ComputerUseSession: ObservableObject { // Handle done/respond completion actions — don't execute, wait for cu_complete if agentAction.type == .done || agentAction.type == .respond { + consecutiveFrontmostBlocks = 0 return } @@ -516,6 +519,8 @@ final class ComputerUseSession: ObservableObject { case .blocked(let reason): log.warning("[\(action.stepNumber)] BLOCKED: \(reason)") + // Verifier blocks are not frontmost-guard blocks — reset the consecutive streak + consecutiveFrontmostBlocks = 0 verifier.recordBlock() if verifier.consecutiveBlockCount >= 3 { isCancelled = true @@ -546,6 +551,17 @@ final class ComputerUseSession: ObservableObject { isCancelled = true state = .failed(reason: "Target app could not be activated after repeated attempts.") logger.finishSession(result: "failed: frontmost guard — too many blocks") + + // Finalize QA recording BEFORE sending abort — the daemon's handleCuSessionAbort + // deletes cuSessionMetadata, so cu_session_finalized must arrive first. + if qaMode { + await finalizeQARecording() + } + do { + try daemonClient.send(CuSessionAbortMessage(sessionId: id)) + } catch { + log.error("Failed to send session abort after frontmost guard limit: \(error)") + } return } let obs = await buildObservation(executionResult: nil, executionError: guardError) From 9390bdbb47948fe46a1e55616af8297ed78ee576 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 23 Feb 2026 20:40:20 -0500 Subject: [PATCH 45/72] feat: enable enforceStartBeforeActions by default Set qaRecording.enforceStartBeforeActions to true in both schema defaults and config defaults, activating fail-closed recording enforcement for QA sessions. Co-Authored-By: Claude Opus 4.6 --- assistant/src/config/defaults.ts | 2 +- assistant/src/config/schema.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assistant/src/config/defaults.ts b/assistant/src/config/defaults.ts index b1722c00081..857bc05441c 100644 --- a/assistant/src/config/defaults.ts +++ b/assistant/src/config/defaults.ts @@ -257,7 +257,7 @@ export const DEFAULT_CONFIG: AssistantConfig = { cleanupIntervalMs: 6 * 60 * 60 * 1000, // 6 hours captureScope: 'display' as const, includeAudio: false, - enforceStartBeforeActions: false, + enforceStartBeforeActions: true, }, sms: { enabled: false, diff --git a/assistant/src/config/schema.ts b/assistant/src/config/schema.ts index e02d940545a..d33ad2168a2 100644 --- a/assistant/src/config/schema.ts +++ b/assistant/src/config/schema.ts @@ -1079,7 +1079,7 @@ export const QaRecordingConfigSchema = z.object({ .default(false), enforceStartBeforeActions: z .boolean({ error: 'qaRecording.enforceStartBeforeActions must be a boolean' }) - .default(false), + .default(true), }); export const SmsConfigSchema = z.object({ @@ -1400,7 +1400,7 @@ export const AssistantConfigSchema = z.object({ cleanupIntervalMs: 6 * 60 * 60 * 1000, captureScope: 'display' as const, includeAudio: false, - enforceStartBeforeActions: false, + enforceStartBeforeActions: true, }), sms: SmsConfigSchema.default({ enabled: false, From e9fddf245a024963483b2e2ebf872f51be514852 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:43:18 -0500 Subject: [PATCH 46/72] fix: set QA latch on user_message path for CU escalation (#7424) The QA latch was only set in handleTaskSubmit and wireEscalationHandler, so user messages sent via the user_message path (handleUserMessage in sessions.ts) never detected QA intent. This meant if a user typed "help me test Slack typing" as a regular chat message, the latch would not be active when the agent later escalated to CU. Add detectQaIntent/detectQaOptOut checks to handleUserMessage so the latch is set/cleared before the message is processed, ensuring subsequent CU escalations correctly inherit QA mode. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../__tests__/qa-latch-user-message.test.ts | 140 ++++++++++++++++++ assistant/src/daemon/handlers/sessions.ts | 14 ++ 2 files changed, 154 insertions(+) create mode 100644 assistant/src/__tests__/qa-latch-user-message.test.ts diff --git a/assistant/src/__tests__/qa-latch-user-message.test.ts b/assistant/src/__tests__/qa-latch-user-message.test.ts new file mode 100644 index 00000000000..104fe241e1c --- /dev/null +++ b/assistant/src/__tests__/qa-latch-user-message.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, test, beforeEach } from 'bun:test'; +import { detectQaIntent, detectQaOptOut } from '../daemon/qa-intent.js'; +import { + setQaLatch, + clearQaLatch, + isQaLatchActive, + qaLatchByConversation, +} from '../daemon/handlers/shared.js'; + +/** + * Tests that the QA latch is correctly set/cleared based on user message + * content, mirroring the logic added to handleUserMessage in sessions.ts. + * + * We test the detection + latch functions directly rather than going through + * the full IPC handler, since the handler simply calls these functions on + * the message content. This avoids the heavy Session/AgentLoop mock setup + * while still verifying the integration contract. + */ +describe('QA latch via user_message path', () => { + const sessionId = 'test-session-1'; + + beforeEach(() => { + // Clear all latch state between tests + qaLatchByConversation.clear(); + }); + + test('user message with QA intent sets the QA latch', () => { + const content = 'help me test Slack typing'; + expect(detectQaIntent(content)).toBe(true); + expect(detectQaOptOut(content)).toBe(false); + + // Simulate what handleUserMessage does: + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + + expect(isQaLatchActive(sessionId)).toBe(true); + }); + + test('user message with opt-out clears the QA latch', () => { + // Pre-set the latch + setQaLatch(sessionId); + expect(isQaLatchActive(sessionId)).toBe(true); + + const content = 'stop qa mode'; + expect(detectQaOptOut(content)).toBe(true); + + // Simulate what handleUserMessage does: + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + + expect(isQaLatchActive(sessionId)).toBe(false); + }); + + test('user message without QA intent does not change the latch', () => { + expect(isQaLatchActive(sessionId)).toBe(false); + + const content = 'Can you check my email?'; + expect(detectQaIntent(content)).toBe(false); + expect(detectQaOptOut(content)).toBe(false); + + // Simulate what handleUserMessage does: + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + + // Latch should remain unset + expect(isQaLatchActive(sessionId)).toBe(false); + }); + + test('opt-out takes priority over QA intent in the same message', () => { + // The current implementation checks opt-out first, so a message that + // somehow matches both patterns should clear rather than set. + setQaLatch(sessionId); + + // "stop testing" matches both detectQaOptOut (/\bstop\s+testing\b/) + // and could theoretically trigger QA patterns. The handler checks + // opt-out first, so the latch should be cleared. + const content = 'stop testing'; + const isOptOut = detectQaOptOut(content); + const isQa = detectQaIntent(content); + + // Simulate what handleUserMessage does (opt-out checked first): + if (isOptOut) { + clearQaLatch(sessionId); + } else if (isQa) { + setQaLatch(sessionId); + } + + expect(isQaLatchActive(sessionId)).toBe(false); + }); + + test('latch persists across multiple non-QA messages', () => { + // Set latch with QA intent + setQaLatch(sessionId); + expect(isQaLatchActive(sessionId)).toBe(true); + + // Subsequent non-QA messages should not affect the latch + const normalMessages = [ + 'What is the weather today?', + 'Send a message to the team', + 'Open the settings page', + ]; + + for (const content of normalMessages) { + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + } + + // Latch should still be active + expect(isQaLatchActive(sessionId)).toBe(true); + }); + + test('empty message content does not affect the latch', () => { + setQaLatch(sessionId); + expect(isQaLatchActive(sessionId)).toBe(true); + + const content = ''; + // In handleUserMessage, empty content skips QA detection entirely + if (content) { + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + } + + expect(isQaLatchActive(sessionId)).toBe(true); + }); +}); diff --git a/assistant/src/daemon/handlers/sessions.ts b/assistant/src/daemon/handlers/sessions.ts index a89938bdb7b..844c8f7e778 100644 --- a/assistant/src/daemon/handlers/sessions.ts +++ b/assistant/src/daemon/handlers/sessions.ts @@ -31,12 +31,15 @@ import { renderHistoryContent, mergeToolResults, pendingStandaloneSecrets, + setQaLatch, + clearQaLatch, type HandlerContext, defineHandlers, type HistoryToolCall, type HistorySurface, type ParsedHistoryMessage, } from './shared.js'; +import { detectQaIntent, detectQaOptOut } from '../qa-intent.js'; import { truncate } from '../../util/truncate.js'; export async function handleUserMessage( @@ -56,6 +59,17 @@ export async function handleUserMessage( wireEscalationHandler(session, socket, ctx); } + // Detect QA intent / opt-out in the user message so the latch is active + // before any subsequent CU escalation within this conversation. + const messageContent = msg.content ?? ''; + if (messageContent) { + if (detectQaOptOut(messageContent)) { + clearQaLatch(msg.sessionId); + } else if (detectQaIntent(messageContent)) { + setQaLatch(msg.sessionId); + } + } + const sendEvent = (event: ServerMessage) => ctx.send(socket, event); // Block inbound messages that contain secrets and redirect to secure prompt From b3c8e0727f7f7084b653bcc0206f81b7e1b7bebe Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:43:32 -0500 Subject: [PATCH 47/72] feat: improve recording gate observability and user feedback (#7425) Add structured logging for CU session initialization, recording gate status transitions, and destructive action blocking. Enhance recording handshake timeout error messages with full session context and actionable diagnostics. Increase Swift overlay error text maxHeight from 60 to 80 and enable text selection on recording warnings. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 36 ++++++++++++++++--- assistant/src/daemon/handlers/computer-use.ts | 14 +++++--- .../Features/Session/SessionOverlayView.swift | 3 +- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 7853489b3f3..465bbee6dc2 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -140,6 +140,18 @@ export class ComputerUseSession { this.targetAppName = targetAppName; this.targetAppBundleId = targetAppBundleId; this.requiresRecording = requiresRecording ?? false; + + log.info( + { + sessionId, + qaMode: !!requiresRecording, + requiresRecording: this.requiresRecording, + targetAppName, + targetAppBundleId, + recordingGateStatus: this.recordingGateStatus, + }, + 'CU session initialized', + ); } // --------------------------------------------------------------------------- @@ -197,11 +209,19 @@ export class ComputerUseSession { this.recordingHandshakeTimer = setTimeout(() => { if (this.recordingGateStatus === 'pending') { log.error( - { sessionId: this.sessionId, timeoutMs: RECORDING_HANDSHAKE_TIMEOUT_MS }, + { + sessionId: this.sessionId, + timeoutMs: RECORDING_HANDSHAKE_TIMEOUT_MS, + recordingGateStatus: this.recordingGateStatus, + requiresRecording: this.requiresRecording, + targetAppName: this.targetAppName, + targetAppBundleId: this.targetAppBundleId, + stepCount: this.stepCount, + }, 'Recording handshake timeout — recording never started', ); this.abortWithError( - 'Recording handshake timed out: recording did not start within 8 seconds. Session aborted to prevent unrecorded actions.', + `Recording handshake timed out after ${RECORDING_HANDSHAKE_TIMEOUT_MS / 1000} seconds. The screen recording did not start. Session aborted because recording is required for QA sessions. Check screen recording permissions in System Settings > Privacy & Security.`, ); } }, RECORDING_HANDSHAKE_TIMEOUT_MS); @@ -692,7 +712,7 @@ export class ComputerUseSession { if (this.requiresRecording && this.recordingGateStatus !== 'started') { if (this.recordingGateStatus === 'failed') { const reason = this.recordingFailureReason ?? 'unknown'; - this.abortWithError(`Recording failed: ${reason}. Session cannot proceed without recording.`); + this.abortWithError(`Recording failed to start: ${reason}. Session aborted because recording is required for QA sessions.`); return { content: `Recording failed: ${reason}. Session aborted.`, isError: true, @@ -700,8 +720,14 @@ export class ComputerUseSession { } if (this.recordingGateStatus === 'pending' && DESTRUCTIVE_TOOLS.has(toolName)) { log.warn( - { sessionId: this.sessionId, toolName, recordingGateStatus: this.recordingGateStatus }, - 'Blocked destructive action — recording has not started yet', + { + sessionId: this.sessionId, + toolName, + recordingGateStatus: this.recordingGateStatus, + requiresRecording: this.requiresRecording, + stepCount: this.stepCount, + }, + 'Blocked destructive action — recording not started', ); return { content: 'Recording has not started yet. Waiting for recording confirmation before executing destructive actions. Use computer_use_wait to wait, or computer_use_done/computer_use_respond if the task can be completed without interaction.', diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index 1cb6865e798..d146f264663 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -389,14 +389,20 @@ function handleCuRecordingStatus(msg: CuRecordingStatus, _socket: net.Socket, ct log.warn({ sessionId: msg.sessionId }, 'CU recording status for unknown session'); return; } - log.info( - { sessionId: msg.sessionId, status: msg.status, reason: msg.reason }, - 'CU recording status update', - ); + const previousStatus = session.recordingGateStatus; session.recordingGateStatus = msg.status; if (msg.status === 'failed' && msg.reason) { session.recordingFailureReason = msg.reason; } + log.info( + { + sessionId: msg.sessionId, + previousStatus, + newStatus: msg.status, + reason: msg.reason, + }, + 'Recording gate status changed', + ); } export const computerUseHandlers = defineHandlers({ diff --git a/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift b/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift index bf55e84913f..8c8e173aa08 100644 --- a/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift +++ b/clients/macos/vellum-assistant/Features/Session/SessionOverlayView.swift @@ -265,8 +265,9 @@ struct SessionOverlayView: View { .foregroundColor(VColor.error) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) } - .frame(maxHeight: 60) + .frame(maxHeight: 80) } .padding(.horizontal, VSpacing.xs) } else if session.isRecordingActive { From 43114122b77deda56cfa845fde9572e8c22dc707 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:43:56 -0500 Subject: [PATCH 48/72] feat: generalize target-app-hints to detect common macOS apps (#7428) Replace the Vellum-only app detection with a table-driven approach that recognizes 20+ common macOS apps (browsers, terminals, IDEs, communication tools, productivity apps, Apple built-ins). Generic words like "notes", "mail", "terminal", "messages", and "settings" require action-verb context (open/launch/test/qa/in/check/use) to avoid false positives. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../src/__tests__/target-app-hints.test.ts | 286 ++++++++++++++++++ assistant/src/daemon/target-app-hints.ts | 175 ++++++++++- 2 files changed, 450 insertions(+), 11 deletions(-) create mode 100644 assistant/src/__tests__/target-app-hints.test.ts diff --git a/assistant/src/__tests__/target-app-hints.test.ts b/assistant/src/__tests__/target-app-hints.test.ts new file mode 100644 index 00000000000..9024cd8f100 --- /dev/null +++ b/assistant/src/__tests__/target-app-hints.test.ts @@ -0,0 +1,286 @@ +import { describe, test, expect } from 'bun:test'; +import { resolveComputerUseTargetAppHint } from '../daemon/target-app-hints.js'; + +describe('resolveComputerUseTargetAppHint', () => { + // ── Vellum (our app) ────────────────────────────────────────────── + describe('Vellum', () => { + test('matches "Vellum app"', () => { + const result = resolveComputerUseTargetAppHint('open the Vellum app'); + expect(result).toEqual({ appName: 'Vellum Assistant', bundleId: 'com.vellum.vellum-assistant' }); + }); + + test('matches "Velly desktop app"', () => { + const result = resolveComputerUseTargetAppHint('use the Velly desktop app'); + expect(result).toEqual({ appName: 'Vellum Assistant', bundleId: 'com.vellum.vellum-assistant' }); + }); + + test('matches "Vellum assistant"', () => { + const result = resolveComputerUseTargetAppHint('test the Vellum assistant'); + expect(result).toEqual({ appName: 'Vellum Assistant', bundleId: 'com.vellum.vellum-assistant' }); + }); + }); + + // ── Browsers ─────────────────────────────────────────────────────── + describe('Browsers', () => { + test('matches "chrome"', () => { + const result = resolveComputerUseTargetAppHint('open chrome and navigate to google.com'); + expect(result).toEqual({ appName: 'Google Chrome', bundleId: 'com.google.Chrome' }); + }); + + test('matches "Google Chrome"', () => { + const result = resolveComputerUseTargetAppHint('use Google Chrome to test the site'); + expect(result).toEqual({ appName: 'Google Chrome', bundleId: 'com.google.Chrome' }); + }); + + test('matches "safari"', () => { + const result = resolveComputerUseTargetAppHint('test in Safari'); + expect(result).toEqual({ appName: 'Safari', bundleId: 'com.apple.Safari' }); + }); + + test('matches "firefox"', () => { + const result = resolveComputerUseTargetAppHint('open Firefox'); + expect(result).toEqual({ appName: 'Firefox', bundleId: 'org.mozilla.firefox' }); + }); + + test('matches "arc browser"', () => { + const result = resolveComputerUseTargetAppHint('switch to Arc browser'); + expect(result).toEqual({ appName: 'Arc', bundleId: 'company.thebrowser.Browser' }); + }); + }); + + // ── Communication ────────────────────────────────────────────────── + describe('Communication', () => { + test('matches "slack"', () => { + const result = resolveComputerUseTargetAppHint('test slack typing'); + expect(result).toEqual({ appName: 'Slack', bundleId: 'com.tinyspeck.slackmacgap' }); + }); + + test('matches "discord"', () => { + const result = resolveComputerUseTargetAppHint('open discord and join the call'); + expect(result).toEqual({ appName: 'Discord', bundleId: 'com.hnc.Discord' }); + }); + + test('matches "zoom"', () => { + const result = resolveComputerUseTargetAppHint('join the zoom meeting'); + expect(result).toEqual({ appName: 'zoom.us', bundleId: 'us.zoom.xos' }); + }); + + test('matches "Microsoft Teams"', () => { + const result = resolveComputerUseTargetAppHint('message them on Microsoft Teams'); + expect(result).toEqual({ appName: 'Microsoft Teams', bundleId: 'com.microsoft.teams2' }); + }); + + test('matches "teams app"', () => { + const result = resolveComputerUseTargetAppHint('open the teams app'); + expect(result).toEqual({ appName: 'Microsoft Teams', bundleId: 'com.microsoft.teams2' }); + }); + }); + + // ── Terminals ────────────────────────────────────────────────────── + describe('Terminals', () => { + test('matches "warp"', () => { + const result = resolveComputerUseTargetAppHint('open warp and run the command'); + expect(result).toEqual({ appName: 'Warp', bundleId: 'dev.warp.Warp-Stable' }); + }); + + test('matches "open Terminal"', () => { + const result = resolveComputerUseTargetAppHint('open terminal and run ls'); + expect(result).toEqual({ appName: 'Terminal', bundleId: 'com.apple.Terminal' }); + }); + + test('matches "in Terminal"', () => { + const result = resolveComputerUseTargetAppHint('run the command in terminal'); + expect(result).toEqual({ appName: 'Terminal', bundleId: 'com.apple.Terminal' }); + }); + + test('matches "iterm"', () => { + const result = resolveComputerUseTargetAppHint('switch to iterm'); + expect(result).toEqual({ appName: 'iTerm', bundleId: 'com.googlecode.iterm2' }); + }); + + test('matches "iterm2"', () => { + const result = resolveComputerUseTargetAppHint('use iterm2 for this'); + expect(result).toEqual({ appName: 'iTerm', bundleId: 'com.googlecode.iterm2' }); + }); + }); + + // ── IDEs ─────────────────────────────────────────────────────────── + describe('IDEs', () => { + test('matches "VS Code"', () => { + const result = resolveComputerUseTargetAppHint('open VS Code'); + expect(result).toEqual({ appName: 'Visual Studio Code', bundleId: 'com.microsoft.VSCode' }); + }); + + test('matches "vscode"', () => { + const result = resolveComputerUseTargetAppHint('open vscode'); + expect(result).toEqual({ appName: 'Visual Studio Code', bundleId: 'com.microsoft.VSCode' }); + }); + + test('matches "Visual Studio Code"', () => { + const result = resolveComputerUseTargetAppHint('use Visual Studio Code'); + expect(result).toEqual({ appName: 'Visual Studio Code', bundleId: 'com.microsoft.VSCode' }); + }); + + test('matches "cursor"', () => { + const result = resolveComputerUseTargetAppHint('open cursor and edit the file'); + expect(result).toEqual({ appName: 'Cursor', bundleId: 'com.todesktop.230313mzl4w4u92' }); + }); + + test('matches "xcode"', () => { + const result = resolveComputerUseTargetAppHint('build the project in xcode'); + expect(result).toEqual({ appName: 'Xcode', bundleId: 'com.apple.dt.Xcode' }); + }); + }); + + // ── Productivity ─────────────────────────────────────────────────── + describe('Productivity', () => { + test('matches "notion"', () => { + const result = resolveComputerUseTargetAppHint('update the page in Notion'); + expect(result).toEqual({ appName: 'Notion', bundleId: 'notion.id' }); + }); + + test('matches "figma"', () => { + const result = resolveComputerUseTargetAppHint('check the design in Figma'); + expect(result).toEqual({ appName: 'Figma', bundleId: 'com.figma.Desktop' }); + }); + + test('matches "finder"', () => { + const result = resolveComputerUseTargetAppHint('browse files in Finder'); + expect(result).toEqual({ appName: 'Finder', bundleId: 'com.apple.finder' }); + }); + }); + + // ── Apple apps (context-required) ────────────────────────────────── + describe('Apple apps (context-required)', () => { + test('"open Notes and write" returns Notes', () => { + const result = resolveComputerUseTargetAppHint('open Notes and write something'); + expect(result).toEqual({ appName: 'Notes', bundleId: 'com.apple.Notes' }); + }); + + test('"in Notes" returns Notes', () => { + const result = resolveComputerUseTargetAppHint('create a list in notes'); + expect(result).toEqual({ appName: 'Notes', bundleId: 'com.apple.Notes' }); + }); + + test('"test Notes" returns Notes', () => { + const result = resolveComputerUseTargetAppHint('test notes search feature'); + expect(result).toEqual({ appName: 'Notes', bundleId: 'com.apple.Notes' }); + }); + + test('"Notes app" returns Notes', () => { + const result = resolveComputerUseTargetAppHint('check the notes app'); + expect(result).toEqual({ appName: 'Notes', bundleId: 'com.apple.Notes' }); + }); + + test('"iMessage" returns Messages', () => { + const result = resolveComputerUseTargetAppHint('send a text via iMessage'); + expect(result).toEqual({ appName: 'Messages', bundleId: 'com.apple.MobileSMS' }); + }); + + test('"open Messages" returns Messages', () => { + const result = resolveComputerUseTargetAppHint('open messages and reply'); + expect(result).toEqual({ appName: 'Messages', bundleId: 'com.apple.MobileSMS' }); + }); + + test('"open Mail" returns Mail', () => { + const result = resolveComputerUseTargetAppHint('open mail and check inbox'); + expect(result).toEqual({ appName: 'Mail', bundleId: 'com.apple.mail' }); + }); + + test('"Mail app" returns Mail', () => { + const result = resolveComputerUseTargetAppHint('use the mail app'); + expect(result).toEqual({ appName: 'Mail', bundleId: 'com.apple.mail' }); + }); + + test('"System Settings" returns System Settings', () => { + const result = resolveComputerUseTargetAppHint('open system settings'); + expect(result).toEqual({ appName: 'System Settings', bundleId: 'com.apple.systempreferences' }); + }); + + test('"System Preferences" returns System Settings', () => { + const result = resolveComputerUseTargetAppHint('check system preferences'); + expect(result).toEqual({ appName: 'System Settings', bundleId: 'com.apple.systempreferences' }); + }); + + test('"check Settings" returns System Settings', () => { + const result = resolveComputerUseTargetAppHint('check settings for accessibility'); + expect(result).toEqual({ appName: 'System Settings', bundleId: 'com.apple.systempreferences' }); + }); + + test('"Settings app" returns System Settings', () => { + const result = resolveComputerUseTargetAppHint('open the settings app'); + expect(result).toEqual({ appName: 'System Settings', bundleId: 'com.apple.systempreferences' }); + }); + }); + + // ── False-positive prevention ────────────────────────────────────── + describe('false positives', () => { + test('"take notes about the meeting" does NOT return Notes', () => { + const result = resolveComputerUseTargetAppHint('take notes about the meeting'); + expect(result).toBeUndefined(); + }); + + test('"write notes for the class" does NOT return Notes', () => { + const result = resolveComputerUseTargetAppHint('write notes for the class'); + expect(result).toBeUndefined(); + }); + + test('"send mail to Bob" does NOT return Mail', () => { + const result = resolveComputerUseTargetAppHint('send mail to Bob'); + expect(result).toBeUndefined(); + }); + + test('"read the messages carefully" does NOT return Messages', () => { + const result = resolveComputerUseTargetAppHint('read the messages carefully'); + expect(result).toBeUndefined(); + }); + + test('"change the settings in the config file" does NOT return System Settings', () => { + const result = resolveComputerUseTargetAppHint('change the settings in the config file'); + expect(result).toBeUndefined(); + }); + + test('"terminal velocity" does NOT return Terminal', () => { + const result = resolveComputerUseTargetAppHint('terminal velocity of the object'); + expect(result).toBeUndefined(); + }); + + test('empty string returns undefined', () => { + const result = resolveComputerUseTargetAppHint(''); + expect(result).toBeUndefined(); + }); + + test('generic text returns undefined', () => { + const result = resolveComputerUseTargetAppHint('do something for me please'); + expect(result).toBeUndefined(); + }); + }); + + // ── Contextual task patterns ─────────────────────────────────────── + describe('contextual task patterns', () => { + test('"test slack typing" returns Slack', () => { + const result = resolveComputerUseTargetAppHint('test slack typing'); + expect(result).toEqual({ appName: 'Slack', bundleId: 'com.tinyspeck.slackmacgap' }); + }); + + test('"QA the discord voice chat" returns Discord', () => { + const result = resolveComputerUseTargetAppHint('QA the discord voice chat'); + expect(result).toEqual({ appName: 'Discord', bundleId: 'com.hnc.Discord' }); + }); + + test('"check chrome rendering" returns Chrome', () => { + const result = resolveComputerUseTargetAppHint('check chrome rendering'); + expect(result).toEqual({ appName: 'Google Chrome', bundleId: 'com.google.Chrome' }); + }); + + test('"launch terminal and run tests" returns Terminal', () => { + const result = resolveComputerUseTargetAppHint('launch terminal and run tests'); + expect(result).toEqual({ appName: 'Terminal', bundleId: 'com.apple.Terminal' }); + }); + + test('"use the terminal app to debug" returns Terminal', () => { + const result = resolveComputerUseTargetAppHint('use the terminal app to debug'); + expect(result).toEqual({ appName: 'Terminal', bundleId: 'com.apple.Terminal' }); + }); + }); +}); diff --git a/assistant/src/daemon/target-app-hints.ts b/assistant/src/daemon/target-app-hints.ts index 64425afc91b..815319baa48 100644 --- a/assistant/src/daemon/target-app-hints.ts +++ b/assistant/src/daemon/target-app-hints.ts @@ -3,25 +3,178 @@ export interface ComputerUseTargetAppHint { bundleId?: string; } +/** + * Context-requiring pattern wrapper: for generic words like "notes", "mail", + * "terminal", "messages", "settings" that could appear in normal sentences, + * we require the word to appear in an app-like context. + * + * Matches patterns like: + * "open Notes", "in Terminal", "test Notes", "Notes app", + * "QA notes search", "launch Terminal" + * + * Does NOT match casual uses like "take notes" or "send mail". + */ +function contextPattern(word: string): RegExp { + // Action verbs / prepositions that signal app-intent. + // Deliberately excludes "the" — too many false positives + // ("the settings in the config", "the messages carefully"). + return new RegExp( + `(?:(?:(?:open|launch|switch\\s+to|in|test|qa|check|use)\\s+)${word}|${word}\\s+app)\\b`, + 'i', + ); +} + +interface AppHintEntry { + patterns: RegExp[]; + appName: string; + bundleId: string; +} + +/** + * Ordered table of app hints. Entries are checked top-to-bottom; first match wins. + * More specific apps (Vellum) come before generic ones. + * + * For unique app names (Slack, Chrome, Discord, etc.), simple word-boundary + * matching is sufficient. For generic words (notes, mail, terminal, messages, + * settings), we use `contextPattern` to avoid false positives like + * "take notes about the meeting" or "send mail to Bob". + */ +export const APP_HINTS: AppHintEntry[] = [ + // Vellum (our app — highest priority) + { + patterns: [/\b(vellum|velly)\s+(desktop\s+)?app\b/, /\b(vellum|velly)\s+assistant\b/], + appName: 'Vellum Assistant', + bundleId: 'com.vellum.vellum-assistant', + }, + // Browsers + { + patterns: [/\bchrome\b/, /\bgoogle\s+chrome\b/], + appName: 'Google Chrome', + bundleId: 'com.google.Chrome', + }, + { + patterns: [/\bsafari\b/], + appName: 'Safari', + bundleId: 'com.apple.Safari', + }, + { + patterns: [/\bfirefox\b/], + appName: 'Firefox', + bundleId: 'org.mozilla.firefox', + }, + { + patterns: [/\barc\s+browser\b/], + appName: 'Arc', + bundleId: 'company.thebrowser.Browser', + }, + // Communication + { + patterns: [/\bslack\b/], + appName: 'Slack', + bundleId: 'com.tinyspeck.slackmacgap', + }, + { + patterns: [/\bdiscord\b/], + appName: 'Discord', + bundleId: 'com.hnc.Discord', + }, + { + patterns: [/\bzoom\b/], + appName: 'zoom.us', + bundleId: 'us.zoom.xos', + }, + { + patterns: [/\bmicrosoft\s+teams\b/, /\bteams\s+app\b/], + appName: 'Microsoft Teams', + bundleId: 'com.microsoft.teams2', + }, + // Terminals + { + patterns: [/\bwarp\b/], + appName: 'Warp', + bundleId: 'dev.warp.Warp-Stable', + }, + { + patterns: [contextPattern('terminal')], + appName: 'Terminal', + bundleId: 'com.apple.Terminal', + }, + { + patterns: [/\biterm2?\b/], + appName: 'iTerm', + bundleId: 'com.googlecode.iterm2', + }, + // IDEs + { + patterns: [/\b(vs\s*code|visual\s+studio\s+code)\b/], + appName: 'Visual Studio Code', + bundleId: 'com.microsoft.VSCode', + }, + { + patterns: [/\bcursor\b/], + appName: 'Cursor', + bundleId: 'com.todesktop.230313mzl4w4u92', + }, + { + patterns: [/\bxcode\b/], + appName: 'Xcode', + bundleId: 'com.apple.dt.Xcode', + }, + // Productivity + { + patterns: [/\bnotion\b/], + appName: 'Notion', + bundleId: 'notion.id', + }, + { + patterns: [/\bfigma\b/], + appName: 'Figma', + bundleId: 'com.figma.Desktop', + }, + { + patterns: [/\bfinder\b/], + appName: 'Finder', + bundleId: 'com.apple.finder', + }, + // Apple apps (generic words — require context) + { + patterns: [contextPattern('notes')], + appName: 'Notes', + bundleId: 'com.apple.Notes', + }, + { + patterns: [contextPattern('messages'), /\bimessage\b/], + appName: 'Messages', + bundleId: 'com.apple.MobileSMS', + }, + { + patterns: [contextPattern('mail')], + appName: 'Mail', + bundleId: 'com.apple.mail', + }, + { + patterns: [/\bsystem\s+settings\b/, /\bsystem\s+preferences\b/, contextPattern('settings')], + appName: 'System Settings', + bundleId: 'com.apple.systempreferences', + }, +]; + /** * Resolve an explicit target app hint from user task text. * This is intentionally conservative: only high-confidence patterns should * lock the CU session to an app. + * + * Iterates through APP_HINTS in order; returns the first match. */ export function resolveComputerUseTargetAppHint(task: string): ComputerUseTargetAppHint | undefined { const normalized = task.toLowerCase(); - // "Vellum app"/"Velly app"/"Vellum assistant" should target the desktop app, - // not similarly named Slack workspaces or Notion pages. - const vellumDesktopMentioned = - /\b(vellum|velly)\s+(desktop\s+)?app\b/.test(normalized) - || /\b(vellum|velly)\s+assistant\b/.test(normalized); - - if (vellumDesktopMentioned) { - return { - appName: 'Vellum Assistant', - bundleId: 'com.vellum.vellum-assistant', - }; + for (const entry of APP_HINTS) { + for (const pattern of entry.patterns) { + if (pattern.test(normalized)) { + return { appName: entry.appName, bundleId: entry.bundleId }; + } + } } return undefined; From 8bc2a6a111bcf08437ba6a1bc27ec97406ea4d37 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:44:41 -0500 Subject: [PATCH 49/72] fix: prevent Vellum from stealing focus during external-app QA sessions (#7431) Gate showMainWindow() calls in CU session lifecycle so they only fire when the target app is Vellum itself or unspecified. For external-app QA sessions (targeting Slack, Chrome, etc.), activating Vellum's main window would steal focus from the app under test, breaking the QA workflow. Also skip the Chrome accessibility restart prompt during external-app sessions since the NSAlert + NSApp.activate would steal focus. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../App/AppDelegate+Sessions.swift | 29 ++++++++++++++++--- .../ComputerUse/Session.swift | 6 ++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 452659d83ef..1dd14fa1263 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -61,6 +61,17 @@ extension AppDelegate { return baseMessage } + // MARK: - External App Target Detection + + /// Returns `true` when the CU session targets an external app (not Vellum + /// itself). When `bundleId` is nil the session has no target constraint and + /// is treated as "self" (backward compatibility). + private func isExternalAppTarget(bundleId: String?) -> Bool { + guard let bundleId, !bundleId.isEmpty else { return false } + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + return bundleId != selfBundleId + } + // MARK: - Accessibility Permission /// Poll for accessibility permission after prompting, giving the user time to grant it in System Settings. @@ -135,8 +146,13 @@ extension AppDelegate { self.textResponseWindow = nil // Keep the main app visible during escalated CU so permission prompts - // and status are always visible to the user. - self.showMainWindow() + // and status are always visible to the user — but only when the + // target app IS Vellum itself (or unspecified). For external-app QA + // sessions (e.g., targeting Slack, Chrome), activating Vellum's main + // window would steal focus from the app under test. + if !self.isExternalAppTarget(bundleId: routed.targetAppBundleId) { + self.showMainWindow() + } await session.run() let endMessage = self.computerUseEndMessage(for: session) @@ -283,8 +299,13 @@ extension AppDelegate { || effectiveTask.localizedCaseInsensitiveContains("test") || effectiveTask.localizedCaseInsensitiveContains("verify") if routed.qaMode == true || looksLikeQaTask { - // QA/test CU sessions should keep the main app open. - self.showMainWindow() + // QA/test CU sessions should keep the main app open — + // but only when the target app is Vellum itself (or + // unspecified). For external-app QA sessions, activating + // Vellum would steal focus from the app under test. + if !self.isExternalAppTarget(bundleId: routed.targetAppBundleId) { + self.showMainWindow() + } } await session.run() let endMessage = self.computerUseEndMessage(for: session) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 0ce3c505b59..01d3e832ccb 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -689,7 +689,13 @@ final class ComputerUseSession: ObservableObject { if let result = enumerator.enumerateCurrentWindow() { // On first step with Chrome: check if web content is visible. + // Skip this check when targeting an external app — showing the + // NSAlert would activate Vellum and steal focus from the app + // under test. + let isExternalTarget = targetAppBundleId != nil + && targetAppBundleId != Bundle.main.bundleIdentifier if !didChromeAccessibilityCheck, + !isExternalTarget, let frontApp = NSWorkspace.shared.frontmostApplication, ChromeAccessibilityHelper.isChromium(frontApp), !ChromeAccessibilityHelper.hasWebContent(elements: result.elements) { From 21ced14eee7b8493e079c868b0653bf5602bb0e1 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:47:44 -0500 Subject: [PATCH 50/72] feat: thread app_bundle_id through open_app tool end-to-end (#7434) The target-app-hints system resolves app names to bundleId pairs, and the Swift openApp() already accepts bundleId, but the value was lost between the daemon and Swift because it was never included in the tool schema or IPC payload. Changes: - Add app_bundle_id as optional param to computer_use_open_app tool definition (definitions.ts + TOOLS.json) - Inject session's targetAppBundleId as default when LLM omits it (computer-use-session.ts) - Add appBundleId field to Swift AgentAction struct (ActionTypes.swift) - Extract app_bundle_id from IPC input dict (Session.swift) - Pass action.appBundleId to openApp() call (ActionExecutor.swift) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/config/bundled-skills/computer-use/TOOLS.json | 4 ++++ assistant/src/daemon/computer-use-session.ts | 5 +++++ assistant/src/tools/computer-use/definitions.ts | 4 ++++ .../macos/vellum-assistant/ComputerUse/ActionExecutor.swift | 2 +- clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift | 3 +++ clients/macos/vellum-assistant/ComputerUse/Session.swift | 3 +++ 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/assistant/src/config/bundled-skills/computer-use/TOOLS.json b/assistant/src/config/bundled-skills/computer-use/TOOLS.json index 98b9dc22d4d..ec2885fc37b 100644 --- a/assistant/src/config/bundled-skills/computer-use/TOOLS.json +++ b/assistant/src/config/bundled-skills/computer-use/TOOLS.json @@ -250,6 +250,10 @@ "type": "string", "description": "The name of the application to open (e.g. \"Slack\", \"Safari\", \"Google Chrome\", \"VS Code\")" }, + "app_bundle_id": { + "type": "string", + "description": "Bundle identifier of the app (e.g. com.apple.Safari). If provided, used for precise app activation." + }, "reasoning": { "type": "string", "description": "Explanation of why you need to open or switch to this app" diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 465bbee6dc2..24e8e665ed8 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -684,6 +684,11 @@ export class ComputerUseSession { isError: true, }; } + + // Inject targetAppBundleId when the LLM didn't provide one + if (!input.app_bundle_id && this.targetAppBundleId) { + input = { ...input, app_bundle_id: this.targetAppBundleId }; + } } if (toolName === 'computer_use_run_applescript') { diff --git a/assistant/src/tools/computer-use/definitions.ts b/assistant/src/tools/computer-use/definitions.ts index b2c5b93c566..39d971d7652 100644 --- a/assistant/src/tools/computer-use/definitions.ts +++ b/assistant/src/tools/computer-use/definitions.ts @@ -302,6 +302,10 @@ export const computerUseOpenAppTool: Tool = { type: 'string', description: 'The name of the application to open (e.g. "Slack", "Safari", "Google Chrome", "VS Code")', }, + app_bundle_id: { + type: 'string', + description: 'Bundle identifier of the app (e.g. com.apple.Safari). If provided, used for precise app activation.', + }, reasoning: { type: 'string', description: 'Explanation of why you need to open or switch to this app', diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index d103f1103c1..8d16686aed8 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -235,7 +235,7 @@ final class ActionExecutor: ActionExecuting { try drag(from: CGPoint(x: fromX, y: fromY), to: CGPoint(x: endX, y: endY)) case .openApp: guard let appName = action.appName else { throw ExecutorError.appNotFound("(no name)") } - try await openApp(name: appName) + try await openApp(name: appName, bundleId: action.appBundleId) case .runAppleScript: guard let source = action.script else { throw ExecutorError.appleScriptMissingScript } return try await runAppleScript(source) diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift b/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift index e92df88654e..1fe556d7dbd 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift @@ -36,6 +36,7 @@ struct AgentAction: Codable { var summary: String? var waitDuration: Int? var appName: String? + var appBundleId: String? var script: String? var reasoning: String var resolvedFromElementId: Int? @@ -56,6 +57,7 @@ struct AgentAction: Codable { summary: String? = nil, waitDuration: Int? = nil, appName: String? = nil, + appBundleId: String? = nil, script: String? = nil, resolvedFromElementId: Int? = nil, resolvedToElementId: Int? = nil, @@ -74,6 +76,7 @@ struct AgentAction: Codable { self.summary = summary self.waitDuration = waitDuration self.appName = appName + self.appBundleId = appBundleId self.script = script self.resolvedFromElementId = resolvedFromElementId self.resolvedToElementId = resolvedToElementId diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 01d3e832ccb..5603bac16cb 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -918,6 +918,8 @@ final class ComputerUseSession: ObservableObject { ?? extractInt(from: msg.input, key: "wait_duration") let appName = msg.input["app_name"]?.value as? String ?? msg.input["appName"]?.value as? String + let appBundleId = msg.input["app_bundle_id"]?.value as? String + ?? msg.input["appBundleId"]?.value as? String let script = msg.input["script"]?.value as? String let elementId = extractInt(from: msg.input, key: "element_id") ?? extractInt(from: msg.input, key: "elementId") @@ -940,6 +942,7 @@ final class ComputerUseSession: ObservableObject { summary: summary, waitDuration: waitDuration, appName: appName, + appBundleId: appBundleId, script: script, resolvedFromElementId: elementId, resolvedToElementId: toElementId, From 89b9145bfcf2a9ecc3f71c2cef3965ed99627fa7 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 20:49:06 -0500 Subject: [PATCH 51/72] fix: harden openApp resolution with expanded aliases and mdfind fallback (#7436) - Expand alias map to cover all apps from target-app-hints.ts APP_HINTS table (Slack, Discord, Arc, Cursor, Warp, Figma, Notion, Notes, Mail, Messages, Finder, System Settings, etc.) - Add mdfind/Spotlight fallback (step 6) for apps installed in non-standard locations (e.g. Homebrew Cask), with 2-second timeout - Extend case-insensitive filesystem search (step 5) to all common app directories (~/Applications, /System/Applications/Utilities), not just /Applications - Add post-activation frontmost verification: after every successful activation, wait 0.5s and log a warning if the expected app did not become frontmost (single verifyFrontmost helper, never throws) - Improve error message on appNotFound to include which resolution paths were attempted for easier debugging Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../ComputerUse/ActionExecutor.swift | 139 ++++++++++++++++-- 1 file changed, 125 insertions(+), 14 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index 8d16686aed8..b31abeceaf5 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -328,16 +328,46 @@ final class ActionExecutor: ActionExecuting { // MARK: - Open App static let appAliases: [String: String] = [ + // Browsers "chrome": "Google Chrome", + "arc": "Arc", + "ff": "Firefox", + "firefox": "Firefox", + "safari": "Safari", + "edge": "Microsoft Edge", + // Communication + "slack": "Slack", + "discord": "Discord", + "zoom": "zoom.us", + "teams": "Microsoft Teams", + "imessage": "Messages", + "messages": "Messages", + "mail": "Mail", + // IDEs & dev tools + "code": "Visual Studio Code", "vs code": "Visual Studio Code", "vscode": "Visual Studio Code", - "edge": "Microsoft Edge", + "cursor": "Cursor", + "xcode": "Xcode", + "warp": "Warp", + "terminal": "Terminal", + "iterm": "iTerm", + // Productivity + "figma": "Figma", + "notion": "Notion", + "notes": "Notes", + "finder": "Finder", + // Microsoft Office "word": "Microsoft Word", "excel": "Microsoft Excel", "powerpoint": "Microsoft PowerPoint", "outlook": "Microsoft Outlook", - "teams": "Microsoft Teams", - "iterm": "iTerm", + // System + "settings": "System Settings", + "preferences": "System Settings", + "system preferences": "System Settings", + "system settings": "System Settings", + // Vellum "vellum": "Vellum Assistant", "velly": "Vellum Assistant", ] @@ -347,17 +377,79 @@ final class ActionExecutor: ActionExecuting { value.lowercased().filter { $0.isLetter || $0.isNumber } } + /// Run `mdfind` as a subprocess with a timeout, returning the first result path or nil. + private func mdfindApp(name: String, timeout: UInt64 = 2_000_000_000) async -> URL? { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind") + process.arguments = [ + "kMDItemKind == 'Application' && kMDItemDisplayName == '\(name)'" + ] + + let stdout = Pipe() + process.standardOutput = stdout + process.standardError = Pipe() // discard stderr + + do { + try process.run() + } catch { + log.warning("openApp mdfind failed to launch: \(error.localizedDescription, privacy: .public)") + return nil + } + + let timeoutTask = Task { + try await Task.sleep(nanoseconds: timeout) + if process.isRunning { + process.terminate() + } + } + + return await withCheckedContinuation { continuation in + process.terminationHandler = { proc in + timeoutTask.cancel() + + guard proc.terminationStatus == 0, + proc.terminationReason != .uncaughtSignal else { + continuation.resume(returning: nil) + return + } + + let data = stdout.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) + + // Take the first result line that ends in .app + if let firstLine = output?.split(separator: "\n").first, + firstLine.hasSuffix(".app") { + continuation.resume(returning: URL(fileURLWithPath: String(firstLine))) + } else { + continuation.resume(returning: nil) + } + } + } + } + + /// After activation, verify the expected app became frontmost and log a warning if not. + private func verifyFrontmost(expectedName: String) async { + try? await Task.sleep(nanoseconds: 500_000_000) // 0.5s + if let frontmost = NSWorkspace.shared.frontmostApplication, + frontmost.localizedName?.lowercased() != expectedName.lowercased() { + log.warning("openApp: app may not have focused — expected \(expectedName, privacy: .public) but frontmost is \(frontmost.localizedName ?? "unknown", privacy: .public)") + } + } + func openApp(name: String, bundleId: String? = nil) async throws { let workspace = NSWorkspace.shared + var attemptedPaths: [String] = [] // 1. Bundle-ID resolution — most precise, avoids name ambiguity if let bundleId, !bundleId.isEmpty { + attemptedPaths.append("bundle-id(\(bundleId))") if let runningApp = workspace.runningApplications.first(where: { $0.bundleIdentifier == bundleId }) { log.info("openApp resolved via bundle-id (running): \(bundleId, privacy: .public)") runningApp.activate() - try await Task.sleep(nanoseconds: 300_000_000) + await verifyFrontmost(expectedName: runningApp.localizedName ?? name) return } if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleId) { @@ -365,6 +457,7 @@ final class ActionExecutor: ActionExecuting { let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) + await verifyFrontmost(expectedName: name) return } log.warning("openApp bundle-id not found, falling back to name: \(bundleId, privacy: .public)") @@ -372,6 +465,7 @@ final class ActionExecutor: ActionExecuting { // 2. Normalized/fuzzy name matching against running apps let normalizedName = Self.normalizeAppName(name) + attemptedPaths.append("fuzzy-running(\(name))") // Guard: when the input normalizes to empty (e.g. "---"), String.contains("") // returns true for any string, which would match the first running app arbitrarily. if !normalizedName.isEmpty, let runningApp = workspace.runningApplications.first(where: { app in @@ -383,7 +477,7 @@ final class ActionExecutor: ActionExecuting { }) { log.info("openApp resolved via fuzzy name match (running): \(name, privacy: .public) -> \(runningApp.localizedName ?? "?", privacy: .public)") runningApp.activate() - try await Task.sleep(nanoseconds: 300_000_000) + await verifyFrontmost(expectedName: runningApp.localizedName ?? name) return } @@ -392,6 +486,7 @@ final class ActionExecutor: ActionExecuting { let resolvedName = Self.appAliases[nameLower] ?? name if resolvedName != name { + attemptedPaths.append("alias(\(nameLower)->\(resolvedName))") log.info("openApp resolved via alias: \(name, privacy: .public) -> \(resolvedName, privacy: .public)") // Re-check running apps with the aliased name let normalizedResolved = Self.normalizeAppName(resolvedName) @@ -400,7 +495,7 @@ final class ActionExecutor: ActionExecuting { return Self.normalizeAppName(localizedName) == normalizedResolved }) { runningApp.activate() - try await Task.sleep(nanoseconds: 300_000_000) + await verifyFrontmost(expectedName: resolvedName) return } } @@ -413,6 +508,7 @@ final class ActionExecutor: ActionExecuting { NSString("~/Applications").expandingTildeInPath, ] + attemptedPaths.append("filesystem-exact(\(searchDirs.joined(separator: ",")))") for dir in searchDirs { let appPath = "\(dir)/\(resolvedName).app" let appURL = URL(fileURLWithPath: appPath) @@ -421,24 +517,39 @@ final class ActionExecutor: ActionExecuting { let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) + await verifyFrontmost(expectedName: resolvedName) return } } - // 5. Case-insensitive filesystem search in /Applications - if let found = try? FileManager.default.contentsOfDirectory(atPath: "/Applications") - .first(where: { - $0.lowercased() == "\(resolvedName.lowercased()).app" - }) { - let appURL = URL(fileURLWithPath: "/Applications/\(found)") - log.info("openApp resolved via filesystem (case-insensitive): \(found, privacy: .public)") + // 5. Case-insensitive filesystem search across all common directories + attemptedPaths.append("filesystem-case-insensitive(\(searchDirs.joined(separator: ",")))") + let targetFilename = "\(resolvedName.lowercased()).app" + for dir in searchDirs { + if let found = try? FileManager.default.contentsOfDirectory(atPath: dir) + .first(where: { $0.lowercased() == targetFilename }) { + let appURL = URL(fileURLWithPath: "\(dir)/\(found)") + log.info("openApp resolved via filesystem (case-insensitive): \(dir)/\(found, privacy: .public)") + let config = NSWorkspace.OpenConfiguration() + config.activates = true + try await workspace.openApplication(at: appURL, configuration: config) + await verifyFrontmost(expectedName: resolvedName) + return + } + } + + // 6. Spotlight/mdfind fallback — catches apps in non-standard locations (e.g. Homebrew Cask) + attemptedPaths.append("mdfind(\(resolvedName))") + if let appURL = await mdfindApp(name: resolvedName) { + log.info("openApp resolved via mdfind: \(appURL.path, privacy: .public)") let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) + await verifyFrontmost(expectedName: resolvedName) return } - log.error("openApp failed: app_not_found name=\(name, privacy: .public) bundleId=\(bundleId ?? "nil", privacy: .public)") + log.error("openApp failed: app_not_found name=\(name, privacy: .public) resolvedName=\(resolvedName, privacy: .public) bundleId=\(bundleId ?? "nil", privacy: .public) attempted=[\(attemptedPaths.joined(separator: ", "), privacy: .public)]") throw ExecutorError.appNotFound(name) } From 4c9d27cb328aa7aa85e21172782f9e0cf3da0c0f Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:35:38 -0500 Subject: [PATCH 52/72] fix: mdfind termination handler ordering and single-quote escaping (#7456) Two fixes in mdfindApp: 1. Register process.terminationHandler BEFORE calling process.run() to prevent a race where mdfind exits before the handler is set, causing withCheckedContinuation to never resume and hang forever. 2. Escape single quotes in app names before interpolating into the mdfind Spotlight query string, preventing query breakage for names like "Kate's App". Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../ComputerUse/ActionExecutor.swift | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index b31abeceaf5..3d6659048c3 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -381,32 +381,21 @@ final class ActionExecutor: ActionExecuting { private func mdfindApp(name: String, timeout: UInt64 = 2_000_000_000) async -> URL? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind") + // Escape single quotes in the app name to prevent breaking the mdfind query + let sanitizedName = name.replacingOccurrences(of: "'", with: "'\\''") process.arguments = [ - "kMDItemKind == 'Application' && kMDItemDisplayName == '\(name)'" + "kMDItemKind == 'Application' && kMDItemDisplayName == '\(sanitizedName)'" ] let stdout = Pipe() process.standardOutput = stdout process.standardError = Pipe() // discard stderr - do { - try process.run() - } catch { - log.warning("openApp mdfind failed to launch: \(error.localizedDescription, privacy: .public)") - return nil - } - - let timeoutTask = Task { - try await Task.sleep(nanoseconds: timeout) - if process.isRunning { - process.terminate() - } - } - return await withCheckedContinuation { continuation in + // Register termination handler BEFORE starting the process to avoid + // a race where mdfind exits before the handler is set, which would + // cause withCheckedContinuation to never resume and hang forever. process.terminationHandler = { proc in - timeoutTask.cancel() - guard proc.terminationStatus == 0, proc.terminationReason != .uncaughtSignal else { continuation.resume(returning: nil) @@ -425,6 +414,23 @@ final class ActionExecutor: ActionExecuting { continuation.resume(returning: nil) } } + + do { + try process.run() + } catch { + // process.run() failed so terminationHandler won't fire — resume manually + log.warning("openApp mdfind failed to launch: \(error.localizedDescription, privacy: .public)") + continuation.resume(returning: nil) + return + } + + // Timeout — terminate the subprocess if it takes too long + Task { + try? await Task.sleep(nanoseconds: timeout) + if process.isRunning { + process.terminate() + } + } } } From dd1e54f7b6a11a3ebf597f3d9533a220eacc92b0 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:35:50 -0500 Subject: [PATCH 53/72] fix: narrow target-app-hints to avoid false-positive matches (#7457) - Add word boundary (\b) before action verb group in contextPattern to prevent partial matches like "login" matching "in" or "domain" matching "in" - Switch Cursor from simple \bcursor\b to contextPattern('cursor') so "move cursor to the submit button" no longer triggers a match - Switch Zoom from simple \bzoom\b to contextPattern('zoom') so "zoom in on the chart" no longer triggers a match - Reorder iTerm before Terminal in APP_HINTS array so "open terminal in iterm2" resolves to iTerm instead of Terminal - Add regression tests for all five false-positive scenarios Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../src/__tests__/target-app-hints.test.ts | 41 +++++++++++++++++-- assistant/src/daemon/target-app-hints.ts | 16 ++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/assistant/src/__tests__/target-app-hints.test.ts b/assistant/src/__tests__/target-app-hints.test.ts index 9024cd8f100..66a1dba602d 100644 --- a/assistant/src/__tests__/target-app-hints.test.ts +++ b/assistant/src/__tests__/target-app-hints.test.ts @@ -60,8 +60,13 @@ describe('resolveComputerUseTargetAppHint', () => { expect(result).toEqual({ appName: 'Discord', bundleId: 'com.hnc.Discord' }); }); - test('matches "zoom"', () => { - const result = resolveComputerUseTargetAppHint('join the zoom meeting'); + test('matches "open zoom"', () => { + const result = resolveComputerUseTargetAppHint('open zoom and join the call'); + expect(result).toEqual({ appName: 'zoom.us', bundleId: 'us.zoom.xos' }); + }); + + test('matches "zoom app"', () => { + const result = resolveComputerUseTargetAppHint('check the zoom app'); expect(result).toEqual({ appName: 'zoom.us', bundleId: 'us.zoom.xos' }); }); @@ -102,6 +107,11 @@ describe('resolveComputerUseTargetAppHint', () => { const result = resolveComputerUseTargetAppHint('use iterm2 for this'); expect(result).toEqual({ appName: 'iTerm', bundleId: 'com.googlecode.iterm2' }); }); + + test('"open terminal in iterm2" resolves to iTerm', () => { + const result = resolveComputerUseTargetAppHint('open terminal in iterm2'); + expect(result).toEqual({ appName: 'iTerm', bundleId: 'com.googlecode.iterm2' }); + }); }); // ── IDEs ─────────────────────────────────────────────────────────── @@ -121,11 +131,16 @@ describe('resolveComputerUseTargetAppHint', () => { expect(result).toEqual({ appName: 'Visual Studio Code', bundleId: 'com.microsoft.VSCode' }); }); - test('matches "cursor"', () => { + test('matches "open cursor"', () => { const result = resolveComputerUseTargetAppHint('open cursor and edit the file'); expect(result).toEqual({ appName: 'Cursor', bundleId: 'com.todesktop.230313mzl4w4u92' }); }); + test('matches "cursor app"', () => { + const result = resolveComputerUseTargetAppHint('check the cursor app'); + expect(result).toEqual({ appName: 'Cursor', bundleId: 'com.todesktop.230313mzl4w4u92' }); + }); + test('matches "xcode"', () => { const result = resolveComputerUseTargetAppHint('build the project in xcode'); expect(result).toEqual({ appName: 'Xcode', bundleId: 'com.apple.dt.Xcode' }); @@ -245,6 +260,26 @@ describe('resolveComputerUseTargetAppHint', () => { expect(result).toBeUndefined(); }); + test('"move cursor to the submit button" does NOT return Cursor', () => { + const result = resolveComputerUseTargetAppHint('move cursor to the submit button'); + expect(result).toBeUndefined(); + }); + + test('"zoom in on the chart" does NOT return Zoom', () => { + const result = resolveComputerUseTargetAppHint('zoom in on the chart'); + expect(result).toBeUndefined(); + }); + + test('"login terminal" does NOT return Terminal (word boundary)', () => { + const result = resolveComputerUseTargetAppHint('login terminal'); + expect(result).toBeUndefined(); + }); + + test('"domain mail server" does NOT return Mail (word boundary)', () => { + const result = resolveComputerUseTargetAppHint('domain mail server'); + expect(result).toBeUndefined(); + }); + test('empty string returns undefined', () => { const result = resolveComputerUseTargetAppHint(''); expect(result).toBeUndefined(); diff --git a/assistant/src/daemon/target-app-hints.ts b/assistant/src/daemon/target-app-hints.ts index 815319baa48..32f0a5a0208 100644 --- a/assistant/src/daemon/target-app-hints.ts +++ b/assistant/src/daemon/target-app-hints.ts @@ -19,7 +19,7 @@ function contextPattern(word: string): RegExp { // Deliberately excludes "the" — too many false positives // ("the settings in the config", "the messages carefully"). return new RegExp( - `(?:(?:(?:open|launch|switch\\s+to|in|test|qa|check|use)\\s+)${word}|${word}\\s+app)\\b`, + `(?:(?:(?:^|\\b)(?:open|launch|switch\\s+to|in|test|qa|check|use)\\s+)${word}|\\b${word}\\s+app)\\b`, 'i', ); } @@ -79,7 +79,7 @@ export const APP_HINTS: AppHintEntry[] = [ bundleId: 'com.hnc.Discord', }, { - patterns: [/\bzoom\b/], + patterns: [contextPattern('zoom')], appName: 'zoom.us', bundleId: 'us.zoom.xos', }, @@ -94,16 +94,16 @@ export const APP_HINTS: AppHintEntry[] = [ appName: 'Warp', bundleId: 'dev.warp.Warp-Stable', }, - { - patterns: [contextPattern('terminal')], - appName: 'Terminal', - bundleId: 'com.apple.Terminal', - }, { patterns: [/\biterm2?\b/], appName: 'iTerm', bundleId: 'com.googlecode.iterm2', }, + { + patterns: [contextPattern('terminal')], + appName: 'Terminal', + bundleId: 'com.apple.Terminal', + }, // IDEs { patterns: [/\b(vs\s*code|visual\s+studio\s+code)\b/], @@ -111,7 +111,7 @@ export const APP_HINTS: AppHintEntry[] = [ bundleId: 'com.microsoft.VSCode', }, { - patterns: [/\bcursor\b/], + patterns: [contextPattern('cursor')], appName: 'Cursor', bundleId: 'com.todesktop.230313mzl4w4u92', }, From c98832f3f90c691c375b3f240da81fe68d2fd4d1 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:35:53 -0500 Subject: [PATCH 54/72] fix: prevent double QA finalization on frontmost guard abort (#7458) When qaMode is true and the frontmost guard hits its 3-consecutive-block limit, finalizeQARecording() and CuSessionAbortMessage were sent inside handleAction, then sent AGAIN by the post-loop code in run(). This caused duplicate cu_session_finalized and cu_session_abort messages to the daemon. Add a didFinalizeQARecording boolean flag that is: - Declared as a private var on the session - Reset to false at the start of run() - Set to true at the end of finalizeQARecording() - Checked in the post-loop to skip redundant finalization and abort Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- clients/macos/vellum-assistant/ComputerUse/Session.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 5603bac16cb..49db57c923d 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -110,6 +110,7 @@ final class ComputerUseSession: ObservableObject { private var consecutiveUnchangedSteps = 0 private var currentStepNumber = 0 private var consecutiveFrontmostBlocks = 0 + private var didFinalizeQARecording = false @Published private(set) var qaRecordingWarningMessage: String? /// Whether screen recording is currently active (for UI indicator). @Published private(set) var isRecordingActive: Bool = false @@ -183,6 +184,7 @@ final class ComputerUseSession: ObservableObject { consecutiveUnchangedSteps = 0 currentStepNumber = 0 consecutiveFrontmostBlocks = 0 + didFinalizeQARecording = false state = .running(step: 0, maxSteps: maxSteps, lastAction: "Starting...", reasoning: "") // QA sessions auto-approve low/medium tools from the start. @@ -422,7 +424,8 @@ final class ComputerUseSession: ObservableObject { cancelSafetyNetTask = nil // Finalize QA recording and send cu_session_finalized - if qaMode { + // Guard: skip if already finalized (e.g. frontmost guard limit triggered finalization early) + if qaMode && !didFinalizeQARecording { await finalizeQARecording() // Send the abort immediately after finalization for cancelled QA sessions. @@ -1313,6 +1316,8 @@ final class ComputerUseSession: ObservableObject { } catch { log.error("QA mode: failed to send cu_session_finalized: \(error.localizedDescription)") } + + didFinalizeQARecording = true } // MARK: - Control From 44f307d87bafeedb027c201441c46622bc171946 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:35:58 -0500 Subject: [PATCH 55/72] fix: defer QA latch updates until message is accepted (#7459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move QA intent/opt-out detection in handleUserMessage to after the secret-ingress and queue-rejection checks. Previously, the latch was mutated before those guards, so a message blocked by checkIngressForSecrets or rejected by enqueueMessage would still flip the QA latch — causing incorrect qaMode/requiresRecording state for subsequent escalations. Also adds two contract tests documenting the invariant that rejected and blocked messages must not mutate the latch. Addresses review feedback from #7424. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../__tests__/qa-latch-user-message.test.ts | 61 +++++++++++++++++-- assistant/src/daemon/handlers/sessions.ts | 24 ++++---- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/assistant/src/__tests__/qa-latch-user-message.test.ts b/assistant/src/__tests__/qa-latch-user-message.test.ts index 104fe241e1c..0c500ea5712 100644 --- a/assistant/src/__tests__/qa-latch-user-message.test.ts +++ b/assistant/src/__tests__/qa-latch-user-message.test.ts @@ -9,12 +9,14 @@ import { /** * Tests that the QA latch is correctly set/cleared based on user message - * content, mirroring the logic added to handleUserMessage in sessions.ts. + * content, mirroring the logic in handleUserMessage in sessions.ts. * - * We test the detection + latch functions directly rather than going through - * the full IPC handler, since the handler simply calls these functions on - * the message content. This avoids the heavy Session/AgentLoop mock setup - * while still verifying the integration contract. + * The handler defers latch updates until after the message has been accepted + * (past secret-ingress blocking and queue-rejection checks). We test the + * detection + latch functions directly rather than going through the full + * IPC handler, since the handler simply calls these functions on the message + * content after acceptance. This avoids the heavy Session/AgentLoop mock + * setup while still verifying the integration contract. */ describe('QA latch via user_message path', () => { const sessionId = 'test-session-1'; @@ -121,6 +123,55 @@ describe('QA latch via user_message path', () => { expect(isQaLatchActive(sessionId)).toBe(true); }); + test('latch is not mutated when message would be rejected (contract test)', () => { + // This test documents the invariant enforced by handleUserMessage: + // QA detection only runs AFTER the message passes secret-ingress and + // queue-rejection checks. A rejected message must not flip the latch. + // + // We simulate by skipping the detection block entirely (as the handler + // does when it returns early on rejection). + expect(isQaLatchActive(sessionId)).toBe(false); + + const content = 'help me test Slack typing'; + expect(detectQaIntent(content)).toBe(true); + + // Simulate rejection: handler returns before reaching QA detection. + const messageRejected = true; + if (!messageRejected) { + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + } + + // Latch must remain unset because the message was rejected + expect(isQaLatchActive(sessionId)).toBe(false); + }); + + test('latch is not mutated when message is blocked by secret ingress (contract test)', () => { + // Similar to the rejection test: if checkIngressForSecrets blocks the + // message, the handler returns early and QA detection never runs. + setQaLatch(sessionId); + expect(isQaLatchActive(sessionId)).toBe(true); + + const content = 'stop qa mode'; + expect(detectQaOptOut(content)).toBe(true); + + // Simulate secret-ingress block: handler returns before QA detection. + const blockedBySecretIngress = true; + if (!blockedBySecretIngress) { + if (detectQaOptOut(content)) { + clearQaLatch(sessionId); + } else if (detectQaIntent(content)) { + setQaLatch(sessionId); + } + } + + // Latch must remain active because the opt-out message was blocked + expect(isQaLatchActive(sessionId)).toBe(true); + }); + test('empty message content does not affect the latch', () => { setQaLatch(sessionId); expect(isQaLatchActive(sessionId)).toBe(true); diff --git a/assistant/src/daemon/handlers/sessions.ts b/assistant/src/daemon/handlers/sessions.ts index 844c8f7e778..a989dddc555 100644 --- a/assistant/src/daemon/handlers/sessions.ts +++ b/assistant/src/daemon/handlers/sessions.ts @@ -59,17 +59,6 @@ export async function handleUserMessage( wireEscalationHandler(session, socket, ctx); } - // Detect QA intent / opt-out in the user message so the latch is active - // before any subsequent CU escalation within this conversation. - const messageContent = msg.content ?? ''; - if (messageContent) { - if (detectQaOptOut(messageContent)) { - clearQaLatch(msg.sessionId); - } else if (detectQaIntent(messageContent)) { - setQaLatch(msg.sessionId); - } - } - const sendEvent = (event: ServerMessage) => ctx.send(socket, event); // Block inbound messages that contain secrets and redirect to secure prompt @@ -110,6 +99,19 @@ export async function handleUserMessage( })); return; } + + // Detect QA intent / opt-out only after the message has been accepted + // (not blocked by secret ingress and not rejected by queue). This prevents + // rejected messages from incorrectly mutating the latch. + const messageContent = msg.content ?? ''; + if (messageContent) { + if (detectQaOptOut(messageContent)) { + clearQaLatch(msg.sessionId); + } else if (detectQaIntent(messageContent)) { + setQaLatch(msg.sessionId); + } + } + if (result.queued) { const position = session.getQueueDepth(); rlog.info({ position }, 'Message queued (session busy)'); From 6705478d11dc2d08babbfbcdbfd30fcfbcd289fb Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:37:06 -0500 Subject: [PATCH 56/72] fix: keep Gmail distinct from Mail in APP_CANONICAL_MAP (#7460) The canonical mapping ['gmail', 'mail'] incorrectly collapsed Gmail and Mail into the same canonical key. This caused taskExplicitlyRequestsCrossApp() to record only one app mention for tasks involving both Gmail and Mail, preventing the cross-app escape path from activating. Change the mapping to ['gmail', 'gmail'] so Gmail retains its own canonical identity, allowing cross-app workflows like "copy from Gmail into Mail" to be detected correctly. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 24e8e665ed8..ac0318ff20c 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -366,7 +366,7 @@ export class ComputerUseSession { ['vellum assistant', 'vellum'], ['visual studio code', 'vscode'], ['iterm2', 'iterm'], - ['gmail', 'mail'], + ['gmail', 'gmail'], ]); /** From cb58b99b532f77ef084ea15046a85cc7c3846f5d Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:37:15 -0500 Subject: [PATCH 57/72] fix: restrict bundle ID injection to target app matches only (#7461) In cross-app workflows (e.g., "copy from Chrome and paste into Safari"), the targetAppBundleId was unconditionally injected when the LLM didn't provide one. This caused the target app's bundle ID to be attached to requests for a different app, leading to wrong app activation since bundle ID takes priority in openApp resolution. Now only injects targetAppBundleId when the requested app matches the target app (or no app name is specified). Addresses review feedback from #7434. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index ac0318ff20c..a257b47d1fd 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -685,8 +685,8 @@ export class ComputerUseSession { }; } - // Inject targetAppBundleId when the LLM didn't provide one - if (!input.app_bundle_id && this.targetAppBundleId) { + // Inject targetAppBundleId only when the requested app matches the target app + if (!input.app_bundle_id && this.targetAppBundleId && (!requestedApp || this.isTargetAppMatch(requestedApp))) { input = { ...input, app_bundle_id: this.targetAppBundleId }; } } From 9a9f092a040ad1906b3adcdebe11e3d74b5ffcd9 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:37:19 -0500 Subject: [PATCH 58/72] fix: pass qaMode explicitly to ComputerUseSession constructor (#7462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The constructor log was deriving qaMode from requiresRecording, but these are independent IPC fields — a session can be in QA mode without requiring recording. Pass qaMode as a separate constructor parameter from msg.qaMode and log it directly. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/computer-use-session.ts | 6 +++++- assistant/src/daemon/handlers/computer-use.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index a257b47d1fd..575e36d03a9 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -108,6 +108,8 @@ export class ComputerUseSession { recordingFailureReason?: string; /** When true, destructive actions are blocked until recording has started. */ private readonly requiresRecording: boolean; + /** Whether this session is operating in QA mode. */ + private readonly qaMode: boolean; /** Timer for the recording handshake timeout. */ private recordingHandshakeTimer: ReturnType | null = null; @@ -127,6 +129,7 @@ export class ComputerUseSession { targetAppName?: string, targetAppBundleId?: string, requiresRecording?: boolean, + qaMode?: boolean, ) { this.sessionId = sessionId; this.task = task; @@ -140,11 +143,12 @@ export class ComputerUseSession { this.targetAppName = targetAppName; this.targetAppBundleId = targetAppBundleId; this.requiresRecording = requiresRecording ?? false; + this.qaMode = qaMode ?? false; log.info( { sessionId, - qaMode: !!requiresRecording, + qaMode: this.qaMode, requiresRecording: this.requiresRecording, targetAppName, targetAppBundleId, diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index d146f264663..dc19f0c88da 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -99,6 +99,7 @@ export function handleCuSessionCreate( msg.targetAppName, msg.targetAppBundleId, msg.requiresRecording, + msg.qaMode, ); sessionRef.current = session; From 1b9bb3f3f9c24310a879fb674c54459d0be5ceca Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:38:04 -0500 Subject: [PATCH 59/72] fix: external target detection fallback and name-based gating (#7463) Add Bundle.main.bundleIdentifier fallback in Session.swift's inline isExternalTarget check so SPM builds (where bundleIdentifier is nil) correctly identify Vellum-targeting sessions instead of always treating them as external. Extend isExternalAppTarget in AppDelegate+Sessions.swift to accept a name parameter alongside bundleId. When bundleId is nil but targetAppName is set (e.g., Slack, Chrome), the name-based check prevents Vellum from stealing focus. Both call sites now pass routed.targetAppName. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../App/AppDelegate+Sessions.swift | 26 +++++++++++++------ .../ComputerUse/Session.swift | 3 ++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 1dd14fa1263..e32322a388f 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -64,12 +64,22 @@ extension AppDelegate { // MARK: - External App Target Detection /// Returns `true` when the CU session targets an external app (not Vellum - /// itself). When `bundleId` is nil the session has no target constraint and - /// is treated as "self" (backward compatibility). - private func isExternalAppTarget(bundleId: String?) -> Bool { - guard let bundleId, !bundleId.isEmpty else { return false } - let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" - return bundleId != selfBundleId + /// itself). Uses bundle ID for precise comparison when available, falls back + /// to name-based check for sessions where only the app name is set. + /// When both are nil the session has no target constraint and is treated as + /// "self" (backward compatibility). + private func isExternalAppTarget(bundleId: String?, name: String?) -> Bool { + // If we have a bundleId, use precise comparison + if let bundleId, !bundleId.isEmpty { + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + return bundleId != selfBundleId + } + // If we only have a name, check it's not Vellum + if let name, !name.isEmpty { + let lower = name.lowercased() + return lower != "vellum" && lower != "vellum assistant" + } + return false } // MARK: - Accessibility Permission @@ -150,7 +160,7 @@ extension AppDelegate { // target app IS Vellum itself (or unspecified). For external-app QA // sessions (e.g., targeting Slack, Chrome), activating Vellum's main // window would steal focus from the app under test. - if !self.isExternalAppTarget(bundleId: routed.targetAppBundleId) { + if !self.isExternalAppTarget(bundleId: routed.targetAppBundleId, name: routed.targetAppName) { self.showMainWindow() } @@ -303,7 +313,7 @@ extension AppDelegate { // but only when the target app is Vellum itself (or // unspecified). For external-app QA sessions, activating // Vellum would steal focus from the app under test. - if !self.isExternalAppTarget(bundleId: routed.targetAppBundleId) { + if !self.isExternalAppTarget(bundleId: routed.targetAppBundleId, name: routed.targetAppName) { self.showMainWindow() } } diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 49db57c923d..8ebdbfa4ee9 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -695,8 +695,9 @@ final class ComputerUseSession: ObservableObject { // Skip this check when targeting an external app — showing the // NSAlert would activate Vellum and steal focus from the app // under test. + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" let isExternalTarget = targetAppBundleId != nil - && targetAppBundleId != Bundle.main.bundleIdentifier + && targetAppBundleId != selfBundleId if !didChromeAccessibilityCheck, !isExternalTarget, let frontApp = NSWorkspace.shared.frontmostApplication, From c11b38c6e3123ee36ba1a8b12cb3ce81d98680e3 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:42:48 -0500 Subject: [PATCH 60/72] fix: use double-quoted Spotlight query for mdfind app names (#7464) Process.arguments bypasses the shell, so the shell-style single-quote escaping ('\'') was passed raw to mdfind, breaking Spotlight query parsing. Switch to double-quoted query values with proper double-quote escaping instead. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../vellum-assistant/ComputerUse/ActionExecutor.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index 3d6659048c3..1de0ff28b31 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -381,10 +381,13 @@ final class ActionExecutor: ActionExecuting { private func mdfindApp(name: String, timeout: UInt64 = 2_000_000_000) async -> URL? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind") - // Escape single quotes in the app name to prevent breaking the mdfind query - let sanitizedName = name.replacingOccurrences(of: "'", with: "'\\''") + // Use double quotes for the display-name value so we only need to escape + // double quotes. Process.arguments bypasses the shell, so shell-style + // single-quote escaping (e.g. '\'') would be passed raw to mdfind and + // break Spotlight query parsing. + let sanitizedName = name.replacingOccurrences(of: "\"", with: "\\\"") process.arguments = [ - "kMDItemKind == 'Application' && kMDItemDisplayName == '\(sanitizedName)'" + "kMDItemKind == 'Application' && kMDItemDisplayName == \"\(sanitizedName)\"" ] let stdout = Pipe() From c8a3c798b46595c40d82878fdc9ea07acb5215df Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:42:54 -0500 Subject: [PATCH 61/72] fix: use defer for didFinalizeQARecording flag in all return paths (#7465) The didFinalizeQARecording flag was only set at the very end of finalizeQARecording(), missing the early return path when recording was required but no artifact was produced. This could cause the post-loop code to call finalizeQARecording() again redundantly. Use a defer block at the top of the method to guarantee the flag is always set regardless of which return path is taken. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- clients/macos/vellum-assistant/ComputerUse/Session.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 8ebdbfa4ee9..ee60fac660a 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -1204,6 +1204,8 @@ final class ComputerUseSession: ObservableObject { /// Stops the screen recorder (if active) and sends a `cu_session_finalized` message to the daemon. private func finalizeQARecording() async { + defer { didFinalizeQARecording = true } + // Map SessionState to a status string let status: String let summary: String @@ -1317,8 +1319,6 @@ final class ComputerUseSession: ObservableObject { } catch { log.error("QA mode: failed to send cu_session_finalized: \(error.localizedDescription)") } - - didFinalizeQARecording = true } // MARK: - Control From 6666a83d7846dfbf52dd4701134abe567ca884cf Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 21:43:21 -0500 Subject: [PATCH 62/72] fix: add name-based fallback to Session.swift isExternalTarget check (#7466) When targetAppBundleId is nil but targetAppName is set (e.g. "Chrome"), the inline isExternalTarget check incorrectly evaluated to false, causing the Chrome accessibility NSAlert to steal focus from the app under test. Now falls back to targetAppName matching, consistent with the external app detection logic in AppDelegate+Sessions.swift. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../vellum-assistant/ComputerUse/Session.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index ee60fac660a..69dea13d784 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -695,9 +695,16 @@ final class ComputerUseSession: ObservableObject { // Skip this check when targeting an external app — showing the // NSAlert would activate Vellum and steal focus from the app // under test. - let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" - let isExternalTarget = targetAppBundleId != nil - && targetAppBundleId != selfBundleId + let isExternalTarget: Bool + if let bundleId = targetAppBundleId, !bundleId.isEmpty { + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + isExternalTarget = bundleId != selfBundleId + } else if let name = targetAppName, !name.isEmpty { + let lower = name.lowercased() + isExternalTarget = lower != "vellum" && lower != "vellum assistant" + } else { + isExternalTarget = false + } if !didChromeAccessibilityCheck, !isExternalTarget, let frontApp = NSWorkspace.shared.frontmostApplication, From 990c616eb89ebe0137b6986d9052f5e4d4b7f583 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 22:25:02 -0500 Subject: [PATCH 63/72] fix: escape backslashes in mdfind Spotlight query sanitization (#7511) Addresses review feedback on PR #7464: the sanitization only escaped double quotes but not backslashes. Since Spotlight's query parser treats `\` as an escape character inside double-quoted strings, an app name containing a backslash would produce a malformed query. Escape backslashes before double quotes to ensure correct nesting. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../vellum-assistant/ComputerUse/ActionExecutor.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index 1de0ff28b31..544c95d215c 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -381,11 +381,12 @@ final class ActionExecutor: ActionExecuting { private func mdfindApp(name: String, timeout: UInt64 = 2_000_000_000) async -> URL? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind") - // Use double quotes for the display-name value so we only need to escape - // double quotes. Process.arguments bypasses the shell, so shell-style + // Use double quotes for the display-name value. Escape backslashes first + // (Spotlight treats \ as an escape character inside double-quoted strings), + // then double quotes. Process.arguments bypasses the shell, so shell-style // single-quote escaping (e.g. '\'') would be passed raw to mdfind and // break Spotlight query parsing. - let sanitizedName = name.replacingOccurrences(of: "\"", with: "\\\"") + let sanitizedName = name.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") process.arguments = [ "kMDItemKind == 'Application' && kMDItemDisplayName == \"\(sanitizedName)\"" ] From 01c86e8e228e923260eb8480b64f330c95d0ae85 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Mon, 23 Feb 2026 22:26:47 -0500 Subject: [PATCH 64/72] fix: escape backslashes in mdfind Spotlight query sanitization (#7515) Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 From 6e39dce52629a307a030684854f1e75a4dd4bc22 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Tue, 24 Feb 2026 00:02:49 -0500 Subject: [PATCH 65/72] feat: strict visual QA mode with focus-lock and recording validation - Add strictVisualQa flag to CU session, IPC contract, and client - Block AppleScript (except target app activation) in strict mode - Block focus-stealing keys (Cmd+H/M/Tab) during focus-lock sessions - Reject completion if target app was never observed frontmost - Add first-frame handshake to ScreenRecorder for capture pipeline validation - Validate salvaged recordings for playability before tracking - Add screen recording permission gate with polling for QA sessions - Suppress title-bar minimize during focus-lock targeting Vellum - Improve activation: use activateIgnoringOtherApps, unhide, deminiaturize - Add requireExactAppMatch to ActionExecutor to prevent fuzzy matching - Tighten QA routing: require positive GUI cue, narrow overly broad patterns - Add clearAllQaLatches and call on session reset to prevent unbounded growth - Respect QA opt-out for latch-based activation in escalation handler - Add Restart menu item to menu bar - Broaden Vellum target-app hint to match bare "vellum" mention - Fix schema migration: storage_kind column to NOT NULL Co-Authored-By: Claude --- assistant/src/daemon/computer-use-session.ts | 73 +++++++- assistant/src/daemon/handlers/computer-use.ts | 1 + assistant/src/daemon/handlers/misc.ts | 3 + assistant/src/daemon/handlers/shared.ts | 26 ++- assistant/src/daemon/ipc-contract.ts | 8 + assistant/src/daemon/qa-intent.ts | 16 +- assistant/src/daemon/server.ts | 3 + assistant/src/daemon/target-app-hints.ts | 4 +- assistant/src/memory/schema-migration.ts | 2 +- .../App/AppDelegate+MenuBar.swift | 5 + .../App/AppDelegate+Sessions.swift | 67 ++++++- .../vellum-assistant/App/AppDelegate.swift | 15 ++ .../ComputerUse/ActionExecutor.swift | 69 ++++++-- .../ComputerUse/ActionTypes.swift | 6 +- .../ComputerUse/ScreenRecorder.swift | 52 ++++++ .../ComputerUse/Session.swift | 167 +++++++++++++++--- .../Features/MainWindow/MainWindow.swift | 10 ++ .../IPC/Generated/IPCContractGenerated.swift | 18 +- clients/shared/IPC/IPCMessages.swift | 8 +- 19 files changed, 482 insertions(+), 71 deletions(-) diff --git a/assistant/src/daemon/computer-use-session.ts b/assistant/src/daemon/computer-use-session.ts index 575e36d03a9..09c1acde74d 100644 --- a/assistant/src/daemon/computer-use-session.ts +++ b/assistant/src/daemon/computer-use-session.ts @@ -110,6 +110,10 @@ export class ComputerUseSession { private readonly requiresRecording: boolean; /** Whether this session is operating in QA mode. */ private readonly qaMode: boolean; + /** When true, target app must be frontmost during interaction and recording must be valid. */ + private readonly strictVisualQa: boolean; + /** Set to true when an observation confirms the target app was frontmost. */ + hasTargetFocusEvidence = false; /** Timer for the recording handshake timeout. */ private recordingHandshakeTimer: ReturnType | null = null; @@ -130,6 +134,7 @@ export class ComputerUseSession { targetAppBundleId?: string, requiresRecording?: boolean, qaMode?: boolean, + strictVisualQa?: boolean, ) { this.sessionId = sessionId; this.task = task; @@ -144,6 +149,7 @@ export class ComputerUseSession { this.targetAppBundleId = targetAppBundleId; this.requiresRecording = requiresRecording ?? false; this.qaMode = qaMode ?? false; + this.strictVisualQa = strictVisualQa ?? false; log.info( { @@ -183,6 +189,17 @@ export class ComputerUseSession { this.previousAXTree = obs.axTree; } + // Track whether the target app was observed frontmost (for strict visual QA). + if (!this.hasTargetFocusEvidence && (obs.frontmostBundleId || obs.frontmostAppName)) { + const bundleMatch = this.targetAppBundleId && obs.frontmostBundleId + && obs.frontmostBundleId === this.targetAppBundleId; + const nameMatch = obs.frontmostAppName && this.isTargetAppMatch(obs.frontmostAppName); + if (bundleMatch || nameMatch) { + this.hasTargetFocusEvidence = true; + log.info({ sessionId: this.sessionId, frontmostAppName: obs.frontmostAppName, frontmostBundleId: obs.frontmostBundleId }, 'Target app focus evidence confirmed'); + } + } + if (this.state === 'awaiting_observation' && this.pendingObservation) { // Resolve the pending proxy tool result with updated screen context const content = this.buildObservationResultContent(obs, hadPreviousAXTree); @@ -349,8 +366,12 @@ export class ComputerUseSession { } private extractAppleScriptActivationTarget(script: string): string | undefined { - const match = /tell\s+application\s+"([^"]+)"\s+to\s+activate/i.exec(script); - return match?.[1]; + // Match `tell application "Name" to activate` + const nameMatch = /tell\s+application\s+"([^"]+)"\s+to\s+activate/i.exec(script); + if (nameMatch) return nameMatch[1]; + // Match `tell application id "bundle.id" to activate` — return bundle ID + const idMatch = /tell\s+application\s+id\s+"([^"]+)"\s+to\s+activate/i.exec(script); + return idMatch?.[1]; } private static normalizeAppLabel(value: string): string { @@ -698,6 +719,33 @@ export class ComputerUseSession { if (toolName === 'computer_use_run_applescript') { const script = typeof input.script === 'string' ? input.script : undefined; const activatedApp = script ? this.extractAppleScriptActivationTarget(script) : undefined; + + // In strict visual QA mode, block ALL AppleScript except activation of the target app itself. + // AppleScript can silently interact with apps in the background, bypassing the visual requirement. + if (this.strictVisualQa) { + // Allow both `tell application "Name" to activate` and `tell application id "bundle.id" to activate` + const isActivationScript = script && ( + /^\s*tell\s+application\s+"[^"]+"\s+to\s+activate\s*$/i.test(script.trim()) + || /^\s*tell\s+application\s+id\s+"[^"]+"\s+to\s+activate\s*$/i.test(script.trim()) + ); + const isTargetByName = activatedApp && this.isTargetAppMatch(activatedApp); + const isTargetByBundleId = this.targetAppBundleId && script + && new RegExp(`tell\\s+application\\s+id\\s+"${this.targetAppBundleId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\s+to\\s+activate`, 'i').test(script); + const isTargetActivation = isActivationScript && (isTargetByName || isTargetByBundleId); + if (!isTargetActivation) { + log.warn({ + sessionId: this.sessionId, + toolName, + strictVisualQa: true, + script: script?.slice(0, 200), + }, 'Blocked AppleScript in strict visual QA mode'); + return { + content: 'Blocked: strict visual QA mode requires all interactions to be visible on screen. AppleScript is not allowed except for activating the target app. Use mouse/keyboard actions instead.', + isError: true, + }; + } + } + if ( activatedApp && !this.isTargetAppMatch(activatedApp) @@ -758,6 +806,27 @@ export class ComputerUseSession { // Check for terminal tools if (toolName === 'computer_use_done' || toolName === 'computer_use_respond') { + // In strict visual QA mode, reject completion if we never observed the target app frontmost. + if (this.strictVisualQa && !this.hasTargetFocusEvidence) { + log.warn({ + sessionId: this.sessionId, + toolName, + targetAppName: this.targetAppName, + targetAppBundleId: this.targetAppBundleId, + }, 'Rejected completion in strict visual QA: target app was never observed frontmost'); + + this.state = 'error'; + const failReason = `Strict visual QA failed: target app "${this.targetAppName ?? this.targetAppBundleId}" was never observed as the frontmost application during the session.`; + this.sendToClient({ + type: 'cu_error', + sessionId: this.sessionId, + message: failReason, + }); + this.abortController?.abort(); + this.notifyTerminal(); + return { content: failReason, isError: true }; + } + const summary = toolName === 'computer_use_done' ? (typeof input.summary === 'string' ? input.summary : 'Task completed') diff --git a/assistant/src/daemon/handlers/computer-use.ts b/assistant/src/daemon/handlers/computer-use.ts index dc19f0c88da..ad4b8ffdb04 100644 --- a/assistant/src/daemon/handlers/computer-use.ts +++ b/assistant/src/daemon/handlers/computer-use.ts @@ -100,6 +100,7 @@ export function handleCuSessionCreate( msg.targetAppBundleId, msg.requiresRecording, msg.qaMode, + msg.strictVisualQa, ); sessionRef.current = session; diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index ab44c951d31..5be41331977 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -105,6 +105,7 @@ export async function handleTaskSubmit( const sessionId = uuid(); const targetApp = resolveComputerUseTargetAppHint(msg.task); const effectiveQa = isQa || (qaLatchActive && !isOptOut); + const strictVisualQa = requiresRecording && !!(targetApp?.bundleId || targetApp?.appName); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId, @@ -116,6 +117,7 @@ export async function handleTaskSubmit( ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), ...(effectiveQa ? { qaMode: true, reportToSessionId: msg.conversationId } : {}), ...(requiresRecording ? { requiresRecording: true } : {}), + ...(strictVisualQa ? { strictVisualQa: true } : {}), }; handleCuSessionCreate(cuMsg, socket, ctx); @@ -132,6 +134,7 @@ export async function handleTaskSubmit( includeAudio: config.qaRecording.includeAudio, } : {}), ...(requiresRecording ? { requiresRecording: true } : {}), + ...(strictVisualQa ? { strictVisualQa: true } : {}), }); } else { // Create text QA session and immediately start processing diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index f97c7acdf93..321c098eaa9 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -36,6 +36,13 @@ export function clearQaLatch(conversationId: string): void { qaLatchByConversation.delete(conversationId); } +/** + * Clear all QA latches (called on session reset / clearAllSessions). + */ +export function clearAllQaLatches(): void { + qaLatchByConversation.clear(); +} + /** * Check whether the QA latch is active for a conversation. */ @@ -269,20 +276,25 @@ export function wireEscalationHandler( const cuSessionId = uuid(); const isQa = detectQaIntent(task); + const isOptOut = detectQaOptOut(task); const qaLatchActive = isQaLatchActive(sourceSessionId); const targetApp = resolveComputerUseTargetAppHint(task); const config = getConfig(); - // Determine whether recording is required: QA intent or latch + config flag - const requiresRecording = (isQa || qaLatchActive) && config.qaRecording.enforceStartBeforeActions; + // Compute effective QA mode — respect opt-out for latch-based activation + const effectiveQa = isQa || (qaLatchActive && !isOptOut); + + // Determine whether recording is required: effective QA + config flag + const requiresRecording = effectiveQa && config.qaRecording.enforceStartBeforeActions; + const strictVisualQa = requiresRecording && !!(targetApp?.bundleId || targetApp?.appName); // Set the QA latch for this conversation when QA intent is detected if (isQa) { setQaLatch(sourceSessionId); } - // Check for explicit opt-out - if (detectQaOptOut(task)) { + // Check for explicit opt-out — clear the latch for future turns + if (isOptOut) { clearQaLatch(sourceSessionId); } @@ -295,8 +307,9 @@ export function wireEscalationHandler( interactionType: 'computer_use', reportToSessionId: sourceSessionId, ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(isQa || qaLatchActive ? { qaMode: true } : {}), + ...(effectiveQa ? { qaMode: true } : {}), ...(requiresRecording ? { requiresRecording: true } : {}), + ...(strictVisualQa ? { strictVisualQa: true } : {}), }; handleCuSessionCreate(cuMsg, currentSocket, ctx); @@ -308,13 +321,14 @@ export function wireEscalationHandler( escalatedFrom: sourceSessionId, reportToSessionId: sourceSessionId, ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(isQa || qaLatchActive ? { + ...(effectiveQa ? { qaMode: true, retentionDays: config.qaRecording.defaultRetentionDays, captureScope: config.qaRecording.captureScope, includeAudio: config.qaRecording.includeAudio, } : {}), ...(requiresRecording ? { requiresRecording: true } : {}), + ...(strictVisualQa ? { strictVisualQa: true } : {}), }); return true; diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 33a0b4528b6..f1f58a8199f 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -187,6 +187,8 @@ export interface CuSessionCreate { targetAppBundleId?: string; /** When true, recording MUST start before any destructive action. */ requiresRecording?: boolean; + /** When true, target app must be visually frontmost during interaction and recording must be valid. */ + strictVisualQa?: boolean; } export interface CuRecordingStatus { @@ -250,6 +252,10 @@ export interface CuObservation { executionError?: string; axTreeBlob?: IpcBlobRef; screenshotBlob?: IpcBlobRef; + /** Name of the frontmost application at observation time. */ + frontmostAppName?: string; + /** Bundle ID of the frontmost application at observation time. */ + frontmostBundleId?: string; } export interface TaskSubmit { @@ -1678,6 +1684,8 @@ export interface TaskRouted { targetAppBundleId?: string; /** When true, recording MUST start before any destructive action. */ requiresRecording?: boolean; + /** When true, target app must be visually frontmost during interaction and recording must be valid. */ + strictVisualQa?: boolean; } export interface RideShotgunProgress { diff --git a/assistant/src/daemon/qa-intent.ts b/assistant/src/daemon/qa-intent.ts index 46aa3847346..391fa8b23f4 100644 --- a/assistant/src/daemon/qa-intent.ts +++ b/assistant/src/daemon/qa-intent.ts @@ -59,16 +59,16 @@ export function shouldRouteQaToComputerUse(taskText: string): boolean { /\bscreen\b/, /\bwindow\b/, /\bcomposer\b/, - /\bthread\b/, - /\bchat\b/, + /\bin\s+the\s+thread\b/, + /\bin\s+the\s+chat\b/, /\bbutton\b/, /\bclick\b/, - /\btype\b/, + /\btype\s+(in|into|text)\b/, /\btyping\b/, /\bscroll\b/, /\bnavigate\b/, - /\bopen\b/, - /\bsend\b/, + /\bopen\s+(the\s+)?(app|window|dialog|menu|browser)\b/, + /\bsend\s+(a\s+)?(message|button|form)\b/, /\bworkflow\b/, /\bbehavior\b/, ]; @@ -91,5 +91,9 @@ export function shouldRouteQaToComputerUse(taskText: string): boolean { if (hasGuiCue) return true; const hasCodeOnlyCue = codeTestCues.some((pattern) => pattern.test(lower)); - return !hasCodeOnlyCue; + if (hasCodeOnlyCue) return false; + + // No positive GUI cue and no code-only cue — don't force CU routing. + // Absence of evidence is not evidence of GUI intent; let the classifier decide. + return false; } diff --git a/assistant/src/daemon/server.ts b/assistant/src/daemon/server.ts index 6d46753502c..33971d1d1a8 100644 --- a/assistant/src/daemon/server.ts +++ b/assistant/src/daemon/server.ts @@ -474,6 +474,9 @@ export class DaemonServer { } this.sessions.clear(); this.sessionOptions.clear(); + // Clear QA latches to prevent unbounded growth + const { clearAllQaLatches } = require('./handlers/shared.js'); + clearAllQaLatches(); return count; } diff --git a/assistant/src/daemon/target-app-hints.ts b/assistant/src/daemon/target-app-hints.ts index 32f0a5a0208..f755e0b097f 100644 --- a/assistant/src/daemon/target-app-hints.ts +++ b/assistant/src/daemon/target-app-hints.ts @@ -42,8 +42,8 @@ interface AppHintEntry { export const APP_HINTS: AppHintEntry[] = [ // Vellum (our app — highest priority) { - patterns: [/\b(vellum|velly)\s+(desktop\s+)?app\b/, /\b(vellum|velly)\s+assistant\b/], - appName: 'Vellum Assistant', + patterns: [/\b(vellum|velly)\s+(desktop\s+)?app\b/, /\b(vellum|velly)\s+assistant\b/, /\bvellum\b/i], + appName: 'Vellum', bundleId: 'com.vellum.vellum-assistant', }, // Browsers diff --git a/assistant/src/memory/schema-migration.ts b/assistant/src/memory/schema-migration.ts index 5b1f6e6cbba..fe9008fc53a 100644 --- a/assistant/src/memory/schema-migration.ts +++ b/assistant/src/memory/schema-migration.ts @@ -647,7 +647,7 @@ export function migrateRemoveAssistantIdColumns(database: Db): void { content_hash TEXT, thumbnail_base64 TEXT, created_at INTEGER NOT NULL, - storage_kind TEXT DEFAULT 'inline_base64', + storage_kind TEXT NOT NULL DEFAULT 'inline_base64', file_path TEXT, sha256 TEXT, expires_at INTEGER diff --git a/clients/macos/vellum-assistant/App/AppDelegate+MenuBar.swift b/clients/macos/vellum-assistant/App/AppDelegate+MenuBar.swift index e0330a1f91d..5069f52f9f1 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+MenuBar.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+MenuBar.swift @@ -250,6 +250,11 @@ extension AppDelegate { menu.addItem(galleryItem) #endif + let restartItem = NSMenuItem(title: "Restart", action: #selector(performRestart), keyEquivalent: "") + restartItem.target = self + restartItem.image = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil) + menu.addItem(restartItem) + menu.addItem(NSMenuItem.separator()) let logoutItem = NSMenuItem(title: "Sign Out", action: #selector(performLogout), keyEquivalent: "") diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index e32322a388f..a2054c455aa 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -82,6 +82,25 @@ extension AppDelegate { return false } + // MARK: - Screen Recording Permission + + /// Poll for screen recording permission after prompting, giving the user time to grant it in System Settings. + private func waitForScreenRecordingPermission() async -> Bool { + // Already granted — no need to prompt or poll + if PermissionManager.screenRecordingStatus() == .granted { return true } + + // Show the OS prompt / open System Settings + PermissionManager.requestScreenRecordingAccess() + + // Poll every 500ms for up to 30 seconds + for _ in 0..<60 { + try? await Task.sleep(nanoseconds: 500_000_000) + if Task.isCancelled { return false } + if PermissionManager.screenRecordingStatus() == .granted { return true } + } + return false + } + // MARK: - Accessibility Permission /// Poll for accessibility permission after prompting, giving the user time to grant it in System Settings. @@ -121,6 +140,23 @@ extension AppDelegate { return } + // Screen recording permission gate — only for QA/recording sessions + if routed.qaMode == true || routed.requiresRecording == true { + guard await waitForScreenRecordingPermission() else { + log.error("Screen Recording permission denied — cannot start QA session \(routed.sessionId)") + do { + try daemonClient.send(CuSessionAbortMessage(sessionId: routed.sessionId)) + } catch { + log.error("Failed to send CU session abort for escalation \(routed.sessionId): \(error)") + } + self.mainWindow?.windowState.showToast( + message: "Computer control requires Screen Recording permission. Grant it in System Settings → Privacy & Security → Screen Recording.", + style: .error + ) + return + } + } + let storedMaxSteps = UserDefaults.standard.integer(forKey: "maxStepsPerSession") let maxSteps = storedMaxSteps > 0 ? storedMaxSteps : 50 let session = ComputerUseSession( @@ -138,7 +174,8 @@ extension AppDelegate { includeAudio: routed.includeAudio ?? false, requiresRecording: routed.requiresRecording ?? false, targetAppName: routed.targetAppName, - targetAppBundleId: routed.targetAppBundleId + targetAppBundleId: routed.targetAppBundleId, + strictVisualQa: routed.strictVisualQa ?? false ) // Don't bind relatedViewModel for escalated sessions — the active view model // may be unrelated if the user switched threads. Tool calls for escalated @@ -164,7 +201,13 @@ extension AppDelegate { self.showMainWindow() } + // Suppress title-bar minimize during focus-lock sessions targeting Vellum + let suppressMinimize = session.strictVisualQa && !self.isExternalAppTarget(bundleId: routed.targetAppBundleId, name: routed.targetAppName) + if suppressMinimize { self.mainWindow?.setSuppressMinimize(true) } + await session.run() + + if suppressMinimize { self.mainWindow?.setSuppressMinimize(false) } let endMessage = self.computerUseEndMessage(for: session) try? await Task.sleep(nanoseconds: 10_000_000_000) overlay.close() @@ -277,6 +320,18 @@ extension AppDelegate { } return } + // Screen recording permission gate — only for QA/recording sessions + if routed.qaMode == true || routed.requiresRecording == true { + guard await self.waitForScreenRecordingPermission() else { + log.error("Screen Recording permission denied — cannot start QA session \(routed.sessionId)") + do { + try self.daemonClient.send(CuSessionAbortMessage(sessionId: routed.sessionId)) + } catch { + log.error("Failed to send CU session abort for \(routed.sessionId): \(error)") + } + return + } + } let storedMaxSteps = UserDefaults.standard.integer(forKey: "maxStepsPerSession") let maxSteps = storedMaxSteps > 0 ? storedMaxSteps : 50 let session = ComputerUseSession( @@ -295,7 +350,8 @@ extension AppDelegate { includeAudio: routed.includeAudio ?? false, requiresRecording: routed.requiresRecording ?? false, targetAppName: routed.targetAppName, - targetAppBundleId: routed.targetAppBundleId + targetAppBundleId: routed.targetAppBundleId, + strictVisualQa: routed.strictVisualQa ?? false ) // Don't bind relatedViewModel — sessions started via startSession() don't // originate from a chat thread, so there's no ChatViewModel to extract @@ -317,7 +373,14 @@ extension AppDelegate { self.showMainWindow() } } + + // Suppress title-bar minimize during focus-lock sessions targeting Vellum + let suppressMinimize = session.strictVisualQa && !self.isExternalAppTarget(bundleId: routed.targetAppBundleId, name: routed.targetAppName) + if suppressMinimize { self.mainWindow?.setSuppressMinimize(true) } + await session.run() + + if suppressMinimize { self.mainWindow?.setSuppressMinimize(false) } let endMessage = self.computerUseEndMessage(for: session) try? await Task.sleep(nanoseconds: 10_000_000_000) overlay.close() diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index ca55b9ef49a..26fee664f5b 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -313,6 +313,21 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { authWindow = window } + @objc func performRestart() { + // Stop daemon before relaunch + assistantCli.stop() + + // Launch a fresh instance then quit + let bundleURL = Bundle.main.bundleURL + let config = NSWorkspace.OpenConfiguration() + config.createsNewApplicationInstance = true + NSWorkspace.shared.openApplication(at: bundleURL, configuration: config) { _, _ in + DispatchQueue.main.async { + NSApp.terminate(nil) + } + } + } + @objc func performLogout() { Task { await authManager.logout() diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index 544c95d215c..54f95a3671e 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -235,7 +235,7 @@ final class ActionExecutor: ActionExecuting { try drag(from: CGPoint(x: fromX, y: fromY), to: CGPoint(x: endX, y: endY)) case .openApp: guard let appName = action.appName else { throw ExecutorError.appNotFound("(no name)") } - try await openApp(name: appName, bundleId: action.appBundleId) + try await openApp(name: appName, bundleId: action.appBundleId, requireExactMatch: action.requireExactAppMatch) case .runAppleScript: guard let source = action.script else { throw ExecutorError.appleScriptMissingScript } return try await runAppleScript(source) @@ -447,7 +447,7 @@ final class ActionExecutor: ActionExecuting { } } - func openApp(name: String, bundleId: String? = nil) async throws { + func openApp(name: String, bundleId: String? = nil, requireExactMatch: Bool = false) async throws { let workspace = NSWorkspace.shared var attemptedPaths: [String] = [] @@ -458,7 +458,18 @@ final class ActionExecutor: ActionExecuting { $0.bundleIdentifier == bundleId }) { log.info("openApp resolved via bundle-id (running): \(bundleId, privacy: .public)") - runningApp.activate() + // For our own app: unhide + deminiaturize to handle hidden/minimized state + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + if bundleId == selfBundleId { + NSApp.unhide(nil) + for window in NSApp.windows where window.isMiniaturized { + window.deminiaturize(nil) + } + } + if runningApp.isHidden { + runningApp.unhide() + } + runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) await verifyFrontmost(expectedName: runningApp.localizedName ?? name) return } @@ -470,25 +481,45 @@ final class ActionExecutor: ActionExecuting { await verifyFrontmost(expectedName: name) return } + if requireExactMatch { + log.error("openApp exact-match mode: bundle-id \(bundleId, privacy: .public) not found, refusing fuzzy fallback") + throw ExecutorError.appNotFound("\(name) (bundle: \(bundleId), exact match required)") + } log.warning("openApp bundle-id not found, falling back to name: \(bundleId, privacy: .public)") + } else if requireExactMatch { + // In exact-match mode without a bundle ID, only allow exact name match + // against running apps — no fuzzy/substring matching + attemptedPaths.append("exact-name(\(name))") + if let runningApp = workspace.runningApplications.first(where: { + $0.localizedName == name + }) { + log.info("openApp resolved via exact name match (running): \(name, privacy: .public)") + runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) + await verifyFrontmost(expectedName: name) + return + } + // Fall through to filesystem/mdfind — those are exact by nature } // 2. Normalized/fuzzy name matching against running apps - let normalizedName = Self.normalizeAppName(name) - attemptedPaths.append("fuzzy-running(\(name))") - // Guard: when the input normalizes to empty (e.g. "---"), String.contains("") - // returns true for any string, which would match the first running app arbitrarily. - if !normalizedName.isEmpty, let runningApp = workspace.runningApplications.first(where: { app in - guard let localizedName = app.localizedName else { return false } - let normalized = Self.normalizeAppName(localizedName) - return normalized == normalizedName - || normalized.contains(normalizedName) - || normalizedName.contains(normalized) - }) { - log.info("openApp resolved via fuzzy name match (running): \(name, privacy: .public) -> \(runningApp.localizedName ?? "?", privacy: .public)") - runningApp.activate() - await verifyFrontmost(expectedName: runningApp.localizedName ?? name) - return + // Skip fuzzy matching in exact-match mode — only bundle-ID and exact name are trusted + if !requireExactMatch { + let normalizedName = Self.normalizeAppName(name) + attemptedPaths.append("fuzzy-running(\(name))") + // Guard: when the input normalizes to empty (e.g. "---"), String.contains("") + // returns true for any string, which would match the first running app arbitrarily. + if !normalizedName.isEmpty, let runningApp = workspace.runningApplications.first(where: { app in + guard let localizedName = app.localizedName else { return false } + let normalized = Self.normalizeAppName(localizedName) + return normalized == normalizedName + || normalized.contains(normalizedName) + || normalizedName.contains(normalized) + }) { + log.info("openApp resolved via fuzzy name match (running): \(name, privacy: .public) -> \(runningApp.localizedName ?? "?", privacy: .public)") + runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) + await verifyFrontmost(expectedName: runningApp.localizedName ?? name) + return + } } // 3. Resolve aliases @@ -504,7 +535,7 @@ final class ActionExecutor: ActionExecuting { guard let localizedName = app.localizedName else { return false } return Self.normalizeAppName(localizedName) == normalizedResolved }) { - runningApp.activate() + runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) await verifyFrontmost(expectedName: resolvedName) return } diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift b/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift index 1fe556d7dbd..b51066684f2 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionTypes.swift @@ -42,6 +42,8 @@ struct AgentAction: Codable { var resolvedFromElementId: Int? var resolvedToElementId: Int? var elementDescription: String? + /// When true, openApp must resolve via bundle ID or exact name only — no fuzzy matching. + var requireExactAppMatch: Bool init( type: ActionType, @@ -61,7 +63,8 @@ struct AgentAction: Codable { script: String? = nil, resolvedFromElementId: Int? = nil, resolvedToElementId: Int? = nil, - elementDescription: String? = nil + elementDescription: String? = nil, + requireExactAppMatch: Bool = false ) { self.type = type self.reasoning = reasoning @@ -81,6 +84,7 @@ struct AgentAction: Codable { self.resolvedFromElementId = resolvedFromElementId self.resolvedToElementId = resolvedToElementId self.elementDescription = elementDescription + self.requireExactAppMatch = requireExactAppMatch } var targetMode: ActionTargetMode { diff --git a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift index 54a102c5068..6b646bbb9af 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift @@ -62,9 +62,16 @@ enum ScreenRecorderError: LocalizedError { protocol ScreenRecording { func startRecording(windowID: CGWindowID?, displayID: CGDirectDisplayID?, includeAudio: Bool) async throws func stopRecording() async throws -> RecordingResult + func waitForFirstFrame(timeoutSeconds: Double) async -> Bool var isRecording: Bool { get } } +extension ScreenRecording { + func waitForFirstFrame(timeoutSeconds: Double = 5.0) async -> Bool { + return true // Mocks assume healthy capture by default + } +} + // MARK: - ScreenRecorder /// Records screen content to an .mp4 file using ScreenCaptureKit (SCStream). @@ -253,6 +260,30 @@ final class ScreenRecorder: NSObject, ScreenRecording { log.info("Screen recording started: \(fileURL.lastPathComponent)") } + // MARK: - First Frame Handshake + + /// Waits for the first video frame to arrive, returning true if received within the timeout. + func waitForFirstFrame(timeoutSeconds: Double = 5.0) async -> Bool { + guard let handler = outputHandler else { return false } + + return await withTaskGroup(of: Bool.self) { group in + group.addTask { + await withCheckedContinuation { continuation in + handler.setFirstFrameContinuation(continuation) + } + return true + } + group.addTask { + try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000)) + handler.cancelFirstFrameWait() + return false + } + let result = await group.next() ?? false + group.cancelAll() + return result + } + } + // MARK: - Stop Recording func stopRecording() async throws -> RecordingResult { @@ -354,6 +385,8 @@ private final class StreamOutputHandler: NSObject, SCStreamOutput, @unchecked Se private let audioInput: AVAssetWriterInput? private var sessionStarted = false private let lock = NSLock() + private var firstFrameContinuation: CheckedContinuation? + private let firstFrameLock = NSLock() init(writer: AVAssetWriter, videoInput: AVAssetWriterInput, audioInput: AVAssetWriterInput?) { self.writer = writer @@ -362,6 +395,18 @@ private final class StreamOutputHandler: NSObject, SCStreamOutput, @unchecked Se super.init() } + func setFirstFrameContinuation(_ continuation: CheckedContinuation?) { + firstFrameLock.lock() + defer { firstFrameLock.unlock() } + firstFrameContinuation = continuation + } + + func cancelFirstFrameWait() { + firstFrameLock.lock() + defer { firstFrameLock.unlock() } + firstFrameContinuation = nil + } + func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) { guard writer.status == .writing else { return } guard sampleBuffer.isValid else { return } @@ -374,6 +419,13 @@ private final class StreamOutputHandler: NSObject, SCStreamOutput, @unchecked Se let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) writer.startSession(atSourceTime: timestamp) sessionStarted = true + + // Signal that the first frame has been received + firstFrameLock.lock() + let cont = firstFrameContinuation + firstFrameContinuation = nil + firstFrameLock.unlock() + cont?.resume() } switch type { diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 69dea13d784..0473eadb4ff 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -2,6 +2,8 @@ import Foundation import VellumAssistantShared import CoreGraphics import AppKit +import AVFoundation +import CoreMedia import os private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "Session") @@ -87,6 +89,8 @@ final class ComputerUseSession: ObservableObject { let targetAppName: String? /// Target app bundle ID for frontmost-app guard — nil means no constraint. let targetAppBundleId: String? + /// When true, enforces that the target app must be visually frontmost during the session. + let strictVisualQa: Bool /// Weak reference to the chat view model for extracting tool calls for notifications. weak var relatedViewModel: ChatViewModel? @@ -143,7 +147,8 @@ final class ComputerUseSession: ObservableObject { includeAudio: Bool = false, requiresRecording: Bool = false, targetAppName: String? = nil, - targetAppBundleId: String? = nil + targetAppBundleId: String? = nil, + strictVisualQa: Bool = false ) { self.id = sessionId ?? UUID().uuidString self.task = task @@ -167,6 +172,7 @@ final class ComputerUseSession: ObservableObject { self.requiresRecording = requiresRecording self.targetAppName = targetAppName self.targetAppBundleId = targetAppBundleId + self.strictVisualQa = strictVisualQa self.verifier = ActionVerifier(maxSteps: maxSteps) self.logger = SessionLogger(task: task, attachments: attachments) } @@ -231,18 +237,42 @@ final class ComputerUseSession: ObservableObject { displayID = CGMainDisplayID() } try await recorder.startRecording(windowID: windowID, displayID: displayID, includeAudio: self.includeAudio) + + // Verify capture pipeline is healthy by waiting for first frame + let firstFrameReceived = await recorder.waitForFirstFrame(timeoutSeconds: 5.0) + if !firstFrameReceived { + log.error("QA mode: first video frame not received within 5 seconds — capture pipeline unhealthy") + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: "First frame not received within 5 seconds")) + if requiresRecording { + state = .failed(reason: "Recording required but capture pipeline failed: no frames received") + qaRecordingWarningMessage = "Recording required but no video frames received" + logger.finishSession(result: "failed: first frame timeout") + cancelSafetyNetTask?.cancel() + cancelSafetyNetTask = nil + await finalizeQARecording() + return + } + qaRecordingWarningMessage = "Recording may be incomplete: capture pipeline was slow to start" + } + log.info("QA mode: screen recording started for session \(self.id) (scope: \(self.captureScope))") isRecordingActive = true // Notify daemon that recording started successfully try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "started")) } catch { - log.error("QA mode: failed to start screen recording: \(error.localizedDescription)") + let reason: String + if let recorderError = error as? ScreenRecorderError { + reason = recorderError.errorDescription ?? error.localizedDescription + } else { + reason = error.localizedDescription + } + log.error("QA mode: failed to start screen recording: \(reason)") // Notify daemon of recording failure - try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: error.localizedDescription)) + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: reason)) if requiresRecording { // Fail-closed: recording is mandatory — abort the session - state = .failed(reason: "Recording required but failed to start: \(error.localizedDescription)") - qaRecordingWarningMessage = "Recording required but failed: \(error.localizedDescription)" + state = .failed(reason: "Recording required but failed to start: \(reason)") + qaRecordingWarningMessage = "Recording required but failed: \(reason)" logger.finishSession(result: "failed: required recording could not start") cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil @@ -250,7 +280,7 @@ final class ComputerUseSession: ObservableObject { return } // Non-fatal for best-effort recording — continue the session without recording - qaRecordingWarningMessage = "Unable to start recording. \(error.localizedDescription)" + qaRecordingWarningMessage = "Unable to start recording. \(reason)" } } @@ -544,16 +574,42 @@ final class ComputerUseSession: ObservableObject { return } + // FOCUS-LOCK KEY BLOCKLIST — in strict visual QA mode, block key combos that + // would steal focus from the target app (hide, minimize, app-switch). + if strictVisualQa, agentAction.type == .key, let keyCombo = agentAction.key?.lowercased() { + let focusStealingKeys: Set = ["cmd+h", "command+h", "cmd+m", "command+m", + "cmd+tab", "command+tab", "cmd+`", "command+`"] + if focusStealingKeys.contains(keyCombo) { + let blockMsg = "Blocked in focus-lock mode: '\(keyCombo)' would steal focus from the target app." + log.warning("[\(action.stepNumber)] Focus-lock key block: \(keyCombo)") + let obs = await buildObservation(executionResult: nil, executionError: blockMsg) + if let obs { + do { try daemonClient.send(obs) } catch { + log.error("Failed to send focus-lock key block observation: \(error)") + } + } + state = .thinking(step: action.stepNumber + 1, maxSteps: maxSteps) + return + } + } + // FRONTMOST-APP GUARD — block destructive actions when the wrong app is in front. // Non-destructive actions (screenshot, open_app, wait, runAppleScript) are allowed // regardless of which app is focused. if let guardError = await checkFrontmostAppGuard(for: agentAction) { consecutiveFrontmostBlocks += 1 log.error("Frontmost guard BLOCKED action \(agentAction.type.rawValue) (\(self.consecutiveFrontmostBlocks) consecutive): \(guardError)") - if consecutiveFrontmostBlocks >= 3 { + + // In strict mode, fail immediately — the guard already retried activation + // internally, so a returned error means focus is unrecoverable. + // In normal mode, allow up to 3 cross-step blocks before failing. + let shouldFail = strictVisualQa || consecutiveFrontmostBlocks >= 3 + if shouldFail { isCancelled = true - state = .failed(reason: "Target app could not be activated after repeated attempts.") - logger.finishSession(result: "failed: frontmost guard — too many blocks") + state = .failed(reason: strictVisualQa + ? "Focus-lock failed: \(guardError)" + : "Target app could not be activated after repeated attempts.") + logger.finishSession(result: "failed: frontmost guard\(strictVisualQa ? " (strict)" : "") — \(guardError)") // Finalize QA recording BEFORE sending abort — the daemon's handleCuSessionAbort // deletes cuSessionMetadata, so cu_session_finalized must arrive first. @@ -635,22 +691,35 @@ final class ComputerUseSession: ObservableObject { // When we have a bundle ID, match on that (most reliable) if let targetBundleId = targetAppBundleId { - let frontmost = NSWorkspace.shared.frontmostApplication - let frontmostBundleId = frontmost?.bundleIdentifier - let frontmostName = frontmost?.localizedName ?? "unknown" + if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == targetBundleId { + return nil + } - guard frontmostBundleId != targetBundleId else { return nil } + let frontmostName = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + let frontmostBundleId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier log.warning("Frontmost guard: frontmost=\(frontmostName) (\(frontmostBundleId ?? "nil")), target=\(self.targetAppName ?? "nil") (\(targetBundleId)). Attempting activation retry.") - // Attempt to activate the target app once - if let targetRunning = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == targetBundleId }) { - targetRunning.activate() - try? await Task.sleep(nanoseconds: 300_000_000) // 300ms for focus to settle - if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == targetBundleId { - log.info("Frontmost guard: activation retry succeeded for \(self.targetAppName ?? targetBundleId)") - return nil + // Retry activation up to 2 times with 300ms settle delay each + let maxRetries = strictVisualQa ? 2 : 1 + for attempt in 1...maxRetries { + // For Vellum target: unhide first since activate() won't work on hidden apps + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + if targetBundleId == selfBundleId { + NSApp.unhide(nil) } + + if let targetRunning = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == targetBundleId }) { + targetRunning.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) + try? await Task.sleep(nanoseconds: 300_000_000) // 300ms for focus to settle + if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == targetBundleId { + log.info("Frontmost guard: activation retry \(attempt) succeeded for \(self.targetAppName ?? targetBundleId)") + return nil + } + } else { + break // Target app not running — no point retrying + } + log.warning("Frontmost guard: activation retry \(attempt)/\(maxRetries) failed") } return "Action blocked: frontmost app is '\(frontmostName)' but target is '\(self.targetAppName ?? targetBundleId)'. Please switch to the target app first." @@ -855,6 +924,11 @@ final class ComputerUseSession: ObservableObject { } } + // Capture frontmost app info for the daemon's focus evidence tracking + let frontmostApp = NSWorkspace.shared.frontmostApplication + let frontmostAppName = frontmostApp?.localizedName + let frontmostBundleId = frontmostApp?.bundleIdentifier + let observation = CuObservationMessage( sessionId: id, axTree: axTreeInline, @@ -870,7 +944,9 @@ final class ComputerUseSession: ObservableObject { executionResult: executionResult, executionError: executionError, axTreeBlob: axTreeBlobRef, - screenshotBlob: screenshotBlobRef + screenshotBlob: screenshotBlobRef, + frontmostAppName: frontmostAppName, + frontmostBundleId: frontmostBundleId ) let screenshotRawBytes = screenshotData?.count ?? 0 @@ -957,7 +1033,8 @@ final class ComputerUseSession: ObservableObject { script: script, resolvedFromElementId: elementId, resolvedToElementId: toElementId, - elementDescription: elementDescription + elementDescription: elementDescription, + requireExactAppMatch: strictVisualQa ) } @@ -1209,6 +1286,32 @@ final class ComputerUseSession: ObservableObject { // MARK: - QA Recording Finalization + /// Validates that a video file is playable by checking for a video track with non-zero dimensions and duration. + private func validateVideoPlayability(at url: URL) async -> Bool { + let asset = AVAsset(url: url) + do { + let tracks = try await asset.load(.tracks) + guard let videoTrack = tracks.first(where: { $0.mediaType == .video }) else { + log.warning("QA mode: salvage video has no video track at \(url.lastPathComponent)") + return false + } + let size = try await videoTrack.load(.naturalSize) + guard size.width > 0, size.height > 0 else { + log.warning("QA mode: salvage video has zero-size video track at \(url.lastPathComponent)") + return false + } + let duration = try await asset.load(.duration) + guard CMTimeGetSeconds(duration) > 0 else { + log.warning("QA mode: salvage video has zero duration at \(url.lastPathComponent)") + return false + } + return true + } catch { + log.warning("QA mode: salvage video validation failed at \(url.lastPathComponent): \(error.localizedDescription)") + return false + } + } + /// Stops the screen recorder (if active) and sends a `cu_session_finalized` message to the daemon. private func finalizeQARecording() async { defer { didFinalizeQARecording = true } @@ -1264,14 +1367,21 @@ final class ComputerUseSession: ObservableObject { try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "stopped")) } catch { isRecordingActive = false - log.error("QA mode: failed to stop screen recording: \(error.localizedDescription)") - try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: error.localizedDescription)) + let reason: String + if let recorderError = error as? ScreenRecorderError { + reason = recorderError.errorDescription ?? error.localizedDescription + } else { + reason = error.localizedDescription + } + log.error("QA mode: failed to stop screen recording: \(reason)") + try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: reason)) if qaRecordingWarningMessage == nil { - qaRecordingWarningMessage = "Unable to finalize recording. \(error.localizedDescription)" + qaRecordingWarningMessage = "Unable to finalize recording. \(reason)" } - // Salvage: if the partial file exists on disk, track it for cleanup + // Salvage: if the partial file exists on disk AND is playable, track it for cleanup if let salvageURL = (recorder as? ScreenRecorder)?.lastRecordingFileURL, - FileManager.default.fileExists(atPath: salvageURL.path) { + FileManager.default.fileExists(atPath: salvageURL.path), + await validateVideoPlayability(at: salvageURL) { let attrs = try? FileManager.default.attributesOfItem(atPath: salvageURL.path) let sizeBytes = (attrs?[.size] as? Int) ?? 0 let expiresAtEpoch = Int(Date().addingTimeInterval(Double(retentionDays) * 24 * 3600).timeIntervalSince1970 * 1000) @@ -1288,6 +1398,9 @@ final class ComputerUseSession: ObservableObject { targetBundleId: nil, expiresAt: expiresAtEpoch ) + } else if let salvageURL = (recorder as? ScreenRecorder)?.lastRecordingFileURL, + FileManager.default.fileExists(atPath: salvageURL.path) { + log.warning("QA mode: salvage recording at \(salvageURL.lastPathComponent) failed playability check — discarding") } } } diff --git a/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift b/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift index 8278022c4f6..d0530f9f1d6 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift @@ -15,6 +15,10 @@ import SwiftUI class TitleBarZoomableWindow: NSWindow { private var preZoomFrame: NSRect? + /// When true, title-bar double-click minimize is suppressed to prevent + /// focus loss during focus-lock QA sessions. + var suppressMinimize = false + /// Weak reference to the composer text view so we can redirect typing to it. weak var composerTextView: NSTextView? @@ -69,6 +73,7 @@ class TitleBarZoomableWindow: NSWindow { let action = UserDefaults.standard.string(forKey: "AppleActionOnDoubleClick") ?? "Maximize" switch action { case "Minimize": + if suppressMinimize { return } miniaturize(nil) case "None": break @@ -347,6 +352,11 @@ final class MainWindow { )) } + /// Suppress or restore title-bar minimize for focus-lock sessions. + func setSuppressMinimize(_ suppress: Bool) { + (window as? TitleBarZoomableWindow)?.suppressMinimize = suppress + } + /// Hide the window without destroying it (can be restored with `show()`). func hide() { window?.orderOut(nil) diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index f9b55fd70ff..7747f4c27e7 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -978,8 +978,12 @@ public struct IPCCuObservation: Codable, Sendable { public let executionError: String? public let axTreeBlob: IPCIpcBlobRef? public let screenshotBlob: IPCIpcBlobRef? + /// Name of the frontmost application at observation time. + public let frontmostAppName: String? + /// Bundle ID of the frontmost application at observation time. + public let frontmostBundleId: String? - public init(type: String, sessionId: String, axTree: String? = nil, axDiff: String? = nil, secondaryWindows: String? = nil, screenshot: String? = nil, screenshotWidthPx: Double? = nil, screenshotHeightPx: Double? = nil, screenWidthPt: Double? = nil, screenHeightPt: Double? = nil, coordinateOrigin: String? = nil, captureDisplayId: Double? = nil, executionResult: String? = nil, executionError: String? = nil, axTreeBlob: IPCIpcBlobRef? = nil, screenshotBlob: IPCIpcBlobRef? = nil) { + public init(type: String, sessionId: String, axTree: String? = nil, axDiff: String? = nil, secondaryWindows: String? = nil, screenshot: String? = nil, screenshotWidthPx: Double? = nil, screenshotHeightPx: Double? = nil, screenWidthPt: Double? = nil, screenHeightPt: Double? = nil, coordinateOrigin: String? = nil, captureDisplayId: Double? = nil, executionResult: String? = nil, executionError: String? = nil, axTreeBlob: IPCIpcBlobRef? = nil, screenshotBlob: IPCIpcBlobRef? = nil, frontmostAppName: String? = nil, frontmostBundleId: String? = nil) { self.type = type self.sessionId = sessionId self.axTree = axTree @@ -996,6 +1000,8 @@ public struct IPCCuObservation: Codable, Sendable { self.executionError = executionError self.axTreeBlob = axTreeBlob self.screenshotBlob = screenshotBlob + self.frontmostAppName = frontmostAppName + self.frontmostBundleId = frontmostBundleId } } @@ -1041,8 +1047,10 @@ public struct IPCCuSessionCreate: Codable, Sendable { public let targetAppBundleId: String? /// When true, recording MUST start before any destructive action. public let requiresRecording: Bool? + /// When true, target app must be visually frontmost during interaction and recording must be valid. + public let strictVisualQa: Bool? - public init(type: String, sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]? = nil, interactionType: String? = nil, reportToSessionId: String? = nil, qaMode: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil, requiresRecording: Bool? = nil) { + public init(type: String, sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]? = nil, interactionType: String? = nil, reportToSessionId: String? = nil, qaMode: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil, requiresRecording: Bool? = nil, strictVisualQa: Bool? = nil) { self.type = type self.sessionId = sessionId self.task = task @@ -1055,6 +1063,7 @@ public struct IPCCuSessionCreate: Codable, Sendable { self.targetAppName = targetAppName self.targetAppBundleId = targetAppBundleId self.requiresRecording = requiresRecording + self.strictVisualQa = strictVisualQa } } @@ -3621,8 +3630,10 @@ public struct IPCTaskRouted: Codable, Sendable { public let targetAppBundleId: String? /// When true, recording MUST start before any destructive action. public let requiresRecording: Bool? + /// When true, target app must be visually frontmost during interaction and recording must be valid. + public let strictVisualQa: Bool? - public init(type: String, sessionId: String, interactionType: String, task: String? = nil, escalatedFrom: String? = nil, qaMode: Bool? = nil, reportToSessionId: String? = nil, retentionDays: Double? = nil, captureScope: String? = nil, includeAudio: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil, requiresRecording: Bool? = nil) { + public init(type: String, sessionId: String, interactionType: String, task: String? = nil, escalatedFrom: String? = nil, qaMode: Bool? = nil, reportToSessionId: String? = nil, retentionDays: Double? = nil, captureScope: String? = nil, includeAudio: Bool? = nil, targetAppName: String? = nil, targetAppBundleId: String? = nil, requiresRecording: Bool? = nil, strictVisualQa: Bool? = nil) { self.type = type self.sessionId = sessionId self.interactionType = interactionType @@ -3636,6 +3647,7 @@ public struct IPCTaskRouted: Codable, Sendable { self.targetAppName = targetAppName self.targetAppBundleId = targetAppBundleId self.requiresRecording = requiresRecording + self.strictVisualQa = strictVisualQa } } diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift index f2a110fd254..23321e6115d 100644 --- a/clients/shared/IPC/IPCMessages.swift +++ b/clients/shared/IPC/IPCMessages.swift @@ -195,7 +195,9 @@ extension IPCCuObservation { executionResult: String?, executionError: String?, axTreeBlob: IPCIpcBlobRef? = nil, - screenshotBlob: IPCIpcBlobRef? = nil + screenshotBlob: IPCIpcBlobRef? = nil, + frontmostAppName: String? = nil, + frontmostBundleId: String? = nil ) { self.init( type: "cu_observation", @@ -213,7 +215,9 @@ extension IPCCuObservation { executionResult: executionResult, executionError: executionError, axTreeBlob: axTreeBlob, - screenshotBlob: screenshotBlob + screenshotBlob: screenshotBlob, + frontmostAppName: frontmostAppName, + frontmostBundleId: frontmostBundleId ) } } From 458446d20060f77fb7d0c267af13f5c62ed1f75a Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Tue, 24 Feb 2026 12:42:32 -0500 Subject: [PATCH 66/72] feat(macos): add AX-first action targeting and focus reliability - Add AXElementRegistry to map element IDs to AXUIElements during AX tree enumeration - Add AXActionExecutor with AX-native click/type/focus via kAXPressAction - Add FocusManager with multi-strategy retry (unhide, activate, AX raise) - Wire AX-first path in Session execute loop with CGEvent fallback - Make openApp fail-closed in strict QA mode via focusAcquireFailed error - Replace inline frontmost guard with FocusManager.acquireVerifiedFocus - Add post-open_app and post-action focus drift detection (terminal in strict mode) - Add FocusReliabilityTests covering registry, focus, executor, and strict QA Co-Authored-By: Claude --- .../ComputerUse/AXActionExecutor.swift | 106 ++++++++ .../ComputerUse/AXElementRegistry.swift | 40 +++ .../ComputerUse/AccessibilityTree.swift | 12 +- .../ComputerUse/ActionExecutor.swift | 58 +++- .../ComputerUse/FocusManager.swift | 225 ++++++++++++++++ .../ComputerUse/Session.swift | 169 ++++++++---- .../FocusReliabilityTests.swift | 254 ++++++++++++++++++ .../vellum-assistantTests/SessionTests.swift | 1 + 8 files changed, 797 insertions(+), 68 deletions(-) create mode 100644 clients/macos/vellum-assistant/ComputerUse/AXActionExecutor.swift create mode 100644 clients/macos/vellum-assistant/ComputerUse/AXElementRegistry.swift create mode 100644 clients/macos/vellum-assistant/ComputerUse/FocusManager.swift create mode 100644 clients/macos/vellum-assistantTests/FocusReliabilityTests.swift diff --git a/clients/macos/vellum-assistant/ComputerUse/AXActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/AXActionExecutor.swift new file mode 100644 index 00000000000..2184bf4fb36 --- /dev/null +++ b/clients/macos/vellum-assistant/ComputerUse/AXActionExecutor.swift @@ -0,0 +1,106 @@ +import ApplicationServices +import AppKit +import os + +private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "AXAction") + +/// Result of an AX-first action attempt. +enum AXActionResult { + /// AX action succeeded — no CGEvent fallback needed. + case success(String?) + /// AX action could not be performed — caller should fall back to CGEvent. + case fallback(reason: String) +} + +/// Executes click and type actions via the Accessibility API instead of CGEvent injection. +/// +/// AX-first actions are more reliable than coordinate-based CGEvent because they: +/// - Target the exact UI element (no coordinate drift from window repositioning) +/// - Work even when the element is partially obscured +/// - Don't require the element to be at a specific screen position +/// +/// Falls back to CGEvent when: +/// - No element registry is available +/// - The target element doesn't support the required AX action +/// - AX API calls fail (app unresponsive, permission issues) +@MainActor +final class AXActionExecutor { + + /// Reference to the element registry for resolving elementId -> AXUIElement. + private let elementRegistry: AXElementRegistry + + init(elementRegistry: AXElementRegistry) { + self.elementRegistry = elementRegistry + } + + /// Attempts to click an element via AX kAXPressAction. + /// + /// - Parameter elementId: The AX element ID from the last enumeration. + /// - Returns: `.success` if the action was performed, `.fallback` if CGEvent should be used. + func click(elementId: Int) -> AXActionResult { + guard let axElement = elementRegistry.resolve(elementId: elementId) else { + return .fallback(reason: "Element [\(elementId)] not in registry") + } + + let result = AXUIElementPerformAction(axElement, kAXPressAction as CFString) + if result == .success { + log.info("AX click on [\(elementId)]: success") + return .success("AX click performed on element [\(elementId)]") + } + + // Some elements don't support kAXPressAction — fall back + log.info("AX click on [\(elementId)]: failed (AXError \(result.rawValue)), falling back to CGEvent") + return .fallback(reason: "kAXPressAction failed: AXError \(result.rawValue)") + } + + /// Attempts to set text on an element via AX kAXValueAttribute. + /// + /// This works for text fields and text areas that accept the AXValue attribute. + /// For elements that don't support it (e.g., content-editable web fields), + /// falls back to CGEvent keyboard input. + /// + /// - Parameters: + /// - elementId: The AX element ID from the last enumeration. + /// - text: The text to set. + /// - Returns: `.success` if the value was set, `.fallback` if CGEvent should be used. + func type(elementId: Int, text: String) -> AXActionResult { + guard let axElement = elementRegistry.resolve(elementId: elementId) else { + return .fallback(reason: "Element [\(elementId)] not in registry") + } + + // First, focus the element + let focusResult = AXUIElementSetAttributeValue(axElement, kAXFocusedAttribute as CFString, true as CFTypeRef) + if focusResult != .success { + log.info("AX type on [\(elementId)]: focus failed (AXError \(focusResult.rawValue)), falling back") + return .fallback(reason: "Could not focus element [\(elementId)]: AXError \(focusResult.rawValue)") + } + + // Try setting the value directly + let result = AXUIElementSetAttributeValue(axElement, kAXValueAttribute as CFString, text as CFTypeRef) + if result == .success { + log.info("AX type on [\(elementId)]: set value successfully (\(text.count) chars)") + return .success("AX type performed on element [\(elementId)]") + } + + log.info("AX type on [\(elementId)]: set value failed (AXError \(result.rawValue)), falling back to CGEvent") + return .fallback(reason: "kAXValueAttribute set failed: AXError \(result.rawValue)") + } + + /// Attempts to focus an element via AX. + /// + /// - Parameter elementId: The AX element ID from the last enumeration. + /// - Returns: `.success` if focused, `.fallback` if it failed. + func focus(elementId: Int) -> AXActionResult { + guard let axElement = elementRegistry.resolve(elementId: elementId) else { + return .fallback(reason: "Element [\(elementId)] not in registry") + } + + let result = AXUIElementSetAttributeValue(axElement, kAXFocusedAttribute as CFString, true as CFTypeRef) + if result == .success { + log.info("AX focus on [\(elementId)]: success") + return .success("AX focus on element [\(elementId)]") + } + + return .fallback(reason: "Could not focus element [\(elementId)]: AXError \(result.rawValue)") + } +} diff --git a/clients/macos/vellum-assistant/ComputerUse/AXElementRegistry.swift b/clients/macos/vellum-assistant/ComputerUse/AXElementRegistry.swift new file mode 100644 index 00000000000..bd733d572f8 --- /dev/null +++ b/clients/macos/vellum-assistant/ComputerUse/AXElementRegistry.swift @@ -0,0 +1,40 @@ +import ApplicationServices +import os + +private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "AXRegistry") + +/// Maps AXElement IDs (from the enumerated AX tree) back to live AXUIElement references. +/// +/// During AX tree enumeration, each element is assigned a monotonic integer ID. This registry +/// stores the original AXUIElement reference for each ID so that AX-first actions (click, type) +/// can target elements directly without coordinate conversion. +/// +/// The registry is cleared and rebuilt on each enumeration cycle, so element IDs are only valid +/// for the current step. +/// +/// Thread safety: accessed from the enumerator (synchronous, single-threaded) and from +/// `AXActionExecutor` (MainActor). All access is sequential — enumeration completes before +/// any action executor reads — so no concurrent mutation occurs. +final class AXElementRegistry: @unchecked Sendable { + /// Maps element ID -> live AXUIElement reference. + private var elements: [Int: AXUIElement] = [:] + + /// Clears all stored references. Called at the start of each enumeration. + func clear() { + elements.removeAll(keepingCapacity: true) + } + + /// Registers an AXUIElement with the given ID. + func register(elementId: Int, element: AXUIElement) { + elements[elementId] = element + } + + /// Resolves an element ID to its live AXUIElement reference. + /// Returns nil if the ID is not in the registry (e.g., stale ID from a previous step). + func resolve(elementId: Int) -> AXUIElement? { + return elements[elementId] + } + + /// The number of registered elements. + var count: Int { elements.count } +} diff --git a/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift b/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift index 29351310498..1f2c7528658 100644 --- a/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift +++ b/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift @@ -28,6 +28,8 @@ struct WindowInfo { protocol AccessibilityTreeProviding { func enumerateCurrentWindow() -> (elements: [AXElement], windowTitle: String, appName: String, pid: pid_t)? func enumerateSecondaryWindows(excludingPID: pid_t?, maxWindows: Int) -> [WindowInfo] + /// Optional element registry — populated during enumeration for AX-first action targeting. + var elementRegistry: AXElementRegistry? { get } } final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { @@ -36,6 +38,9 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { /// correct app when our own window is frontmost. private var lastTargetPid: pid_t? + /// Element registry for AX-first action targeting — maps elementId -> AXUIElement. + let elementRegistry: AXElementRegistry? = AXElementRegistry() + /// Track total elements enumerated in current call to prevent infinite loops private var totalElementsEnumerated = 0 /// Maximum elements to enumerate before bailing out (protects against circular refs) @@ -147,11 +152,12 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { nextId = 1 totalElementsEnumerated = 0 + elementRegistry?.clear() let elements = enumerateElementSafely(element: windowElement, depth: 0, maxDepth: 25) let flat = AccessibilityTreeEnumerator.flattenElements(elements) let interactive = flat.filter { Self.interactiveRoles.contains($0.role) } - log.info("Enumerated \(appName, privacy: .public): \(flat.count) total, \(interactive.count) interactive, maxId=\(self.nextId - 1)") + log.info("Enumerated \(appName, privacy: .public): \(flat.count) total, \(interactive.count) interactive, maxId=\(self.nextId - 1), registry=\(self.elementRegistry?.count ?? 0)") lastTargetPid = pid return (elements: elements, windowTitle: windowTitle, appName: appName, pid: pid) @@ -216,6 +222,7 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { nextId = 1 totalElementsEnumerated = 0 + elementRegistry?.clear() let elements = enumerateElementSafely(element: windowElement, depth: 0, maxDepth: 25) guard !elements.isEmpty else { return nil } @@ -335,6 +342,7 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { if isInteractive { let id = nextId nextId += 1 + elementRegistry?.register(elementId: id, element: element) return [AXElement( id: id, role: role, @@ -354,6 +362,7 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { if isStaticText && hasTextContent { let id = nextId nextId += 1 + elementRegistry?.register(elementId: id, element: element) return [AXElement( id: id, role: role, @@ -373,6 +382,7 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { if isContainer && !childElements.isEmpty { let id = nextId nextId += 1 + elementRegistry?.register(elementId: id, element: element) return [AXElement( id: id, role: role, diff --git a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift index dc677eca7cb..5e0b8e37a03 100644 --- a/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/ActionExecutor.swift @@ -21,6 +21,7 @@ enum ExecutorError: LocalizedError { case appleScriptMissingScript case appleScriptTimeout case clipboardMismatch + case focusAcquireFailed(String) var errorDescription: String? { switch self { @@ -36,6 +37,7 @@ enum ExecutorError: LocalizedError { case .appleScriptMissingScript: return "run_applescript requires a script" case .appleScriptTimeout: return "AppleScript timed out after 5 seconds" case .clipboardMismatch: return "Clipboard contents changed before paste injection; aborting to prevent wrong text from being typed" + case .focusAcquireFailed(let reason): return "FOCUS_ACQUIRE_FAILED: \(reason)" } } } @@ -44,8 +46,6 @@ protocol ActionExecuting { func execute(_ action: AgentAction) async throws -> String? } -private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "ActionExecutor") - final class ActionExecutor: ActionExecuting { private let eventSource: CGEventSource? @@ -387,8 +387,8 @@ final class ActionExecutor: ActionExecuting { "system preferences": "System Settings", "system settings": "System Settings", // Vellum - "vellum": "Vellum Assistant", - "velly": "Vellum Assistant", + "vellum": "Vellum", + "velly": "Vellum", ] /// Strips non-alphanumeric characters and lowercases for fuzzy comparison. @@ -457,13 +457,17 @@ final class ActionExecutor: ActionExecuting { } } - /// After activation, verify the expected app became frontmost and log a warning if not. - private func verifyFrontmost(expectedName: String) async { + /// After activation, verify the expected app became frontmost. + /// Returns true if the expected app is frontmost, false otherwise. + @discardableResult + private func verifyFrontmost(expectedName: String) async -> Bool { try? await Task.sleep(nanoseconds: 500_000_000) // 0.5s if let frontmost = NSWorkspace.shared.frontmostApplication, frontmost.localizedName?.lowercased() != expectedName.lowercased() { log.warning("openApp: app may not have focused — expected \(expectedName, privacy: .public) but frontmost is \(frontmost.localizedName ?? "unknown", privacy: .public)") + return false } + return true } func openApp(name: String, bundleId: String? = nil, requireExactMatch: Bool = false) async throws { @@ -489,7 +493,11 @@ final class ActionExecutor: ActionExecuting { runningApp.unhide() } runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) - await verifyFrontmost(expectedName: runningApp.localizedName ?? name) + let focused = await verifyFrontmost(expectedName: runningApp.localizedName ?? name) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(runningApp.localizedName ?? name)' but frontmost is '\(frontmost)'") + } return } if let appURL = workspace.urlForApplication(withBundleIdentifier: bundleId) { @@ -497,7 +505,11 @@ final class ActionExecutor: ActionExecuting { let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) - await verifyFrontmost(expectedName: name) + let focused = await verifyFrontmost(expectedName: name) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(name)' but frontmost is '\(frontmost)'") + } return } if requireExactMatch { @@ -514,7 +526,11 @@ final class ActionExecutor: ActionExecuting { }) { log.info("openApp resolved via exact name match (running): \(name, privacy: .public)") runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) - await verifyFrontmost(expectedName: name) + let focused = await verifyFrontmost(expectedName: name) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(name)' but frontmost is '\(frontmost)'") + } return } // Fall through to filesystem/mdfind — those are exact by nature @@ -555,7 +571,11 @@ final class ActionExecutor: ActionExecuting { return Self.normalizeAppName(localizedName) == normalizedResolved }) { runningApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) - await verifyFrontmost(expectedName: resolvedName) + let focused = await verifyFrontmost(expectedName: resolvedName) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(resolvedName)' but frontmost is '\(frontmost)'") + } return } } @@ -577,7 +597,11 @@ final class ActionExecutor: ActionExecuting { let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) - await verifyFrontmost(expectedName: resolvedName) + let focused = await verifyFrontmost(expectedName: resolvedName) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(resolvedName)' but frontmost is '\(frontmost)'") + } return } } @@ -593,7 +617,11 @@ final class ActionExecutor: ActionExecuting { let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) - await verifyFrontmost(expectedName: resolvedName) + let focused = await verifyFrontmost(expectedName: resolvedName) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(resolvedName)' but frontmost is '\(frontmost)'") + } return } } @@ -605,7 +633,11 @@ final class ActionExecutor: ActionExecuting { let config = NSWorkspace.OpenConfiguration() config.activates = true try await workspace.openApplication(at: appURL, configuration: config) - await verifyFrontmost(expectedName: resolvedName) + let focused = await verifyFrontmost(expectedName: resolvedName) + if requireExactMatch && !focused { + let frontmost = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + throw ExecutorError.focusAcquireFailed("Expected '\(resolvedName)' but frontmost is '\(frontmost)'") + } return } diff --git a/clients/macos/vellum-assistant/ComputerUse/FocusManager.swift b/clients/macos/vellum-assistant/ComputerUse/FocusManager.swift new file mode 100644 index 00000000000..8ed26446e71 --- /dev/null +++ b/clients/macos/vellum-assistant/ComputerUse/FocusManager.swift @@ -0,0 +1,225 @@ +import AppKit +import ApplicationServices +import os + +private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "FocusManager") + +/// Centralized focus acquisition with multi-strategy retry and AX-level window raise. +/// +/// Strategies (in order): +/// 1. Unhide the target app (handles NSApp.hide / isHidden state) +/// 2. NSRunningApplication.activate(options:) with .activateIgnoringOtherApps +/// 3. AX-level kAXRaiseAction on the main/focused window (bypasses WM quirks) +/// 4. Verify frontmost app matches target +/// +/// Each attempt includes a settle delay to let the window server process the switch. +@MainActor +final class FocusManager { + + /// Result of a focus acquisition attempt. + enum FocusResult { + case success + case targetNotRunning + case failed(reason: String) + } + + /// The settle delay after each activation attempt (in nanoseconds). + private static let settleDelayNs: UInt64 = 300_000_000 // 300ms + + /// Attempts to bring the target app to the foreground and verify it's frontmost. + /// + /// - Parameters: + /// - bundleId: Target app's bundle identifier (preferred, most reliable). + /// - appName: Target app's display name (fallback when no bundle ID). + /// - maxRetries: Maximum activation attempts (default 2 for strict, 1 for normal). + /// - Returns: `.success` if the target is frontmost, or a failure reason. + func acquireVerifiedFocus( + bundleId: String?, + appName: String?, + maxRetries: Int = 2 + ) async -> FocusResult { + let frontmostBefore = NSWorkspace.shared.frontmostApplication + log.info("Focus: acquiring target=\(bundleId ?? "nil", privacy: .public)/\(appName ?? "nil", privacy: .public) frontmostBefore=\(frontmostBefore?.localizedName ?? "nil", privacy: .public)(\(frontmostBefore?.bundleIdentifier ?? "nil", privacy: .public))") + + // Fast path: already frontmost + if isFrontmost(bundleId: bundleId, appName: appName) { + log.info("Focus: already frontmost — no activation needed") + return .success + } + + // Resolve the running application (deterministic: prefers visible-window instance) + guard let targetApp = resolveRunningApp(bundleId: bundleId, appName: appName) else { + log.warning("Focus: target not running bundleId=\(bundleId ?? "nil", privacy: .public) appName=\(appName ?? "nil", privacy: .public)") + return .targetNotRunning + } + + let displayName = targetApp.localizedName ?? appName ?? bundleId ?? "unknown" + let targetPID = targetApp.processIdentifier + log.info("Focus: resolved target=\(displayName, privacy: .public) pid=\(targetPID)") + + for attempt in 1...maxRetries { + // Step 1: Unhide if hidden + if targetApp.isHidden { + targetApp.unhide() + log.info("Focus[\(attempt)]: unhid \(displayName, privacy: .public)") + } + + // Special handling for our own app (LSUIElement / .accessory policy) + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + if targetApp.bundleIdentifier == selfBundleId { + NSApp.unhide(nil) + for window in NSApp.windows where window.isMiniaturized { + window.deminiaturize(nil) + } + } + + // Step 2: NSRunningApplication.activate + targetApp.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) + + // Step 3: AX-level window raise — more reliable for stubborn windows + raiseMainWindowViaAX(pid: targetPID) + + // Settle delay + try? await Task.sleep(nanoseconds: Self.settleDelayNs) + + // Step 4: Verify + if isFrontmost(bundleId: bundleId, appName: appName) { + let frontmostAfter = NSWorkspace.shared.frontmostApplication + log.info("Focus[\(attempt)]: verified \(displayName, privacy: .public) is frontmost pid=\(targetPID) frontmostAfter=\(frontmostAfter?.localizedName ?? "nil", privacy: .public)") + return .success + } + + let currentFrontmost = NSWorkspace.shared.frontmostApplication + log.warning("Focus[\(attempt)/\(maxRetries)]: \(displayName, privacy: .public) not frontmost after activation — frontmost is \(currentFrontmost?.localizedName ?? "unknown", privacy: .public)(\(currentFrontmost?.bundleIdentifier ?? "nil", privacy: .public))") + } + + let frontmostName = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" + let frontmostBid = NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "nil" + let reason = "Could not activate '\(displayName)' (pid=\(targetPID)) after \(maxRetries) attempts. Frontmost is '\(frontmostName)' (\(frontmostBid))." + log.error("Focus: FAILED — \(reason, privacy: .public)") + return .failed(reason: reason) + } + + // MARK: - Private Helpers + + /// Checks whether the frontmost app matches the target by bundle ID or name. + private func isFrontmost(bundleId: String?, appName: String?) -> Bool { + guard let frontmost = NSWorkspace.shared.frontmostApplication else { return false } + + if let bid = bundleId, !bid.isEmpty { + return frontmost.bundleIdentifier == bid + } + if let name = appName, !name.isEmpty { + return frontmost.localizedName == name + } + // No target constraint — always matches + return true + } + + /// Finds the NSRunningApplication for the target. + /// + /// When multiple instances match (e.g., two Chrome instances), prefers the one + /// whose PID owns a visible layer-0 window (determined via `CGWindowListCopyWindowInfo`). + /// This avoids picking a headless/background instance that can't be focused. + private func resolveRunningApp(bundleId: String?, appName: String?) -> NSRunningApplication? { + let workspace = NSWorkspace.shared + + let candidates: [NSRunningApplication] + if let bid = bundleId, !bid.isEmpty { + candidates = workspace.runningApplications.filter { $0.bundleIdentifier == bid } + } else if let name = appName, !name.isEmpty { + candidates = workspace.runningApplications.filter { $0.localizedName == name } + } else { + return nil + } + + guard !candidates.isEmpty else { return nil } + if candidates.count == 1 { return candidates.first } + + // Multiple instances — prefer the one with a visible layer-0 window + let visiblePIDs = Self.pidsWithVisibleWindows() + log.info("Focus: \(candidates.count) candidate instances, visiblePIDs=\(visiblePIDs)") + + for candidate in candidates { + if visiblePIDs.contains(candidate.processIdentifier) { + log.info("Focus: chose pid=\(candidate.processIdentifier) (has visible window)") + return candidate + } + } + + // No candidate has a visible window — fall back to first + log.info("Focus: no candidate has visible window, using first (pid=\(candidates[0].processIdentifier))") + return candidates.first + } + + /// Returns PIDs that own at least one visible, layer-0 (normal) window. + private static func pidsWithVisibleWindows() -> Set { + guard let windowList = CGWindowListCopyWindowInfo( + [.optionOnScreenOnly, .excludeDesktopElements], + kCGNullWindowID + ) as? [[String: Any]] else { + return [] + } + + var pids = Set() + for info in windowList { + guard let pid = info[kCGWindowOwnerPID as String] as? pid_t, + let layer = info[kCGWindowLayer as String] as? Int, layer == 0 else { + continue + } + pids.insert(pid) + } + return pids + } + + /// Uses AX APIs to raise the main/focused window of the target app. + /// + /// This is more reliable than NSRunningApplication.activate() for apps that + /// have multiple windows or that the WM doesn't bring to front on activate alone. + /// + /// Sequence: + /// 1. Get focused or main window + /// 2. Set kAXMainAttribute on the window (make it the main window) + /// 3. Perform kAXRaiseAction (bring window to front) + /// 4. Set kAXFocusedAttribute on the app element (transfer keyboard focus) + private func raiseMainWindowViaAX(pid: pid_t) { + let appElement = AXUIElementCreateApplication(pid) + AXUIElementSetMessagingTimeout(appElement, 3.0) + + // Try focused window first, fall back to main window + var windowRef: CFTypeRef? + var result = AXUIElementCopyAttributeValue(appElement, kAXFocusedWindowAttribute as CFString, &windowRef) + if result != .success { + result = AXUIElementCopyAttributeValue(appElement, kAXMainWindowAttribute as CFString, &windowRef) + } + + guard result == .success, + let windowValue = windowRef, + CFGetTypeID(windowValue) == AXUIElementGetTypeID() else { + log.debug("AX raise: no focused/main window for pid \(pid)") + return + } + + let window = windowValue as! AXUIElement + + // Set as main window + let mainResult = AXUIElementSetAttributeValue(window, kAXMainAttribute as CFString, kCFBooleanTrue) + if mainResult != .success { + log.debug("AX raise: kAXMainAttribute set failed for pid \(pid): \(mainResult.rawValue)") + } + + // Raise the window to front + let raiseResult = AXUIElementPerformAction(window, kAXRaiseAction as CFString) + if raiseResult == .success { + log.debug("AX raise: raised window for pid \(pid)") + } else { + log.debug("AX raise: kAXRaiseAction failed for pid \(pid): \(raiseResult.rawValue)") + } + + // Set focused attribute on the app element to transfer keyboard focus + let focusResult = AXUIElementSetAttributeValue(appElement, kAXFocusedAttribute as CFString, kCFBooleanTrue) + if focusResult != .success { + log.debug("AX raise: kAXFocusedAttribute set failed for pid \(pid): \(focusResult.rawValue)") + } + } +} diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 0473eadb4ff..bf9491c70a9 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -107,6 +107,11 @@ final class ComputerUseSession: ObservableObject { private let verifier: ActionVerifier private let logger: SessionLogger private let initialDelayMs: UInt64 + private let focusManager = FocusManager() + private lazy var axActionExecutor: AXActionExecutor? = { + guard let registry = enumerator.elementRegistry else { return nil } + return AXActionExecutor(elementRegistry: registry) + }() private var didChromeAccessibilityCheck = false private var previousAXTreeText: String? private var previousElements: [AXElement]? @@ -636,20 +641,104 @@ final class ComputerUseSession: ObservableObject { } consecutiveFrontmostBlocks = 0 - // EXECUTE + // EXECUTE — try AX-first for element-targeted actions, fall back to CGEvent var executionResult: String? = nil var executionError: String? = nil - do { - executionResult = try await executor.execute(agentAction) - } catch { - let errorMessage = error.localizedDescription - // AppleScript errors are non-fatal — let the daemon adapt - if agentAction.type == .runAppleScript { - log.warning("[\(action.stepNumber)] AppleScript error (non-fatal): \(errorMessage)") - executionError = errorMessage - } else { - executionError = errorMessage + var usedAXPath = false + if let axExecutor = axActionExecutor, let elementId = agentAction.resolvedFromElementId { + let axResult: AXActionResult + switch agentAction.type { + case .click, .doubleClick, .rightClick: + axResult = axExecutor.click(elementId: elementId) + case .type: + if let text = agentAction.text { + axResult = axExecutor.type(elementId: elementId, text: text) + } else { + axResult = .fallback(reason: "No text provided") + } + default: + axResult = .fallback(reason: "Action type \(agentAction.type.rawValue) not supported via AX") + } + + switch axResult { + case .success(let result): + executionResult = result + usedAXPath = true + log.info("[\(action.stepNumber)] AX-first action succeeded for [\(elementId)]") + case .fallback(let reason): + log.info("[\(action.stepNumber)] AX-first fallback: \(reason, privacy: .public)") + // Fall through to CGEvent execution below + } + } + + if !usedAXPath { + do { + executionResult = try await executor.execute(agentAction) + } catch { + let errorMessage = error.localizedDescription + + // AppleScript errors are non-fatal — let the daemon adapt + if agentAction.type == .runAppleScript { + log.warning("[\(action.stepNumber)] AppleScript error (non-fatal): \(errorMessage)") + executionError = errorMessage + } else { + executionError = errorMessage + } + } + } + + // POST-OPEN_APP FOCUS VERIFICATION — in strict mode, use FocusManager to + // confirm the opened app is actually frontmost after openApp execution. + if strictVisualQa && agentAction.type == .openApp && executionError == nil { + let openedAppBundleId = agentAction.appBundleId + let openedAppName = agentAction.appName + if openedAppBundleId != nil || openedAppName != nil { + let openAppFocus = await focusManager.acquireVerifiedFocus( + bundleId: openedAppBundleId, + appName: openedAppName, + maxRetries: 2 + ) + if case .failed(let reason) = openAppFocus { + log.error("[\(action.stepNumber)] Strict QA: open_app focus verification failed: \(reason, privacy: .public)") + isCancelled = true + state = .failed(reason: "FOCUS_ACQUIRE_FAILED: open_app '\(openedAppName ?? openedAppBundleId ?? "unknown")' — \(reason)") + logger.finishSession(result: "failed: open_app focus verification — \(reason)") + if qaMode { + await finalizeQARecording() + } + do { + try daemonClient.send(CuSessionAbortMessage(sessionId: id)) + } catch { + log.error("Failed to send session abort after open_app focus failure: \(error)") + } + return + } + } + } + + // POST-EXECUTION FOCUS VERIFICATION — in strict mode, verify target app + // is still frontmost after the action. In strict mode, drift is terminal. + if strictVisualQa && (targetAppBundleId != nil || targetAppName != nil) { + let postActionFocus = await focusManager.acquireVerifiedFocus( + bundleId: targetAppBundleId, + appName: targetAppName, + maxRetries: 1 + ) + if case .failed(let reason) = postActionFocus { + log.error("[\(action.stepNumber)] Strict QA: post-action focus drift detected: \(reason, privacy: .public)") + isCancelled = true + state = .failed(reason: "FOCUS_ACQUIRE_FAILED: post-action drift — \(reason)") + logger.finishSession(result: "failed: post-action focus drift — \(reason)") + if qaMode { + await finalizeQARecording() + } + do { + try daemonClient.send(CuSessionAbortMessage(sessionId: id)) + } catch { + log.error("Failed to send session abort after post-action focus drift: \(error)") + } + return } } @@ -689,52 +778,24 @@ final class ComputerUseSession: ObservableObject { let destructiveTypes: Set = [.click, .doubleClick, .rightClick, .type, .key, .scroll, .drag] guard destructiveTypes.contains(action.type) else { return nil } - // When we have a bundle ID, match on that (most reliable) - if let targetBundleId = targetAppBundleId { - if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == targetBundleId { - return nil - } - - let frontmostName = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" - let frontmostBundleId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier + // No target constraint — always pass + guard targetAppBundleId != nil || targetAppName != nil else { return nil } - log.warning("Frontmost guard: frontmost=\(frontmostName) (\(frontmostBundleId ?? "nil")), target=\(self.targetAppName ?? "nil") (\(targetBundleId)). Attempting activation retry.") - - // Retry activation up to 2 times with 300ms settle delay each - let maxRetries = strictVisualQa ? 2 : 1 - for attempt in 1...maxRetries { - // For Vellum target: unhide first since activate() won't work on hidden apps - let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" - if targetBundleId == selfBundleId { - NSApp.unhide(nil) - } - - if let targetRunning = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == targetBundleId }) { - targetRunning.activate(options: [.activateIgnoringOtherApps, .activateAllWindows]) - try? await Task.sleep(nanoseconds: 300_000_000) // 300ms for focus to settle - if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == targetBundleId { - log.info("Frontmost guard: activation retry \(attempt) succeeded for \(self.targetAppName ?? targetBundleId)") - return nil - } - } else { - break // Target app not running — no point retrying - } - log.warning("Frontmost guard: activation retry \(attempt)/\(maxRetries) failed") - } - - return "Action blocked: frontmost app is '\(frontmostName)' but target is '\(self.targetAppName ?? targetBundleId)'. Please switch to the target app first." - } - - // Fall back to name-based matching when no bundle ID is available - if let targetName = targetAppName { - let frontmostName = NSWorkspace.shared.frontmostApplication?.localizedName ?? "unknown" - guard frontmostName != targetName else { return nil } + let maxRetries = strictVisualQa ? 2 : 1 + let result = await focusManager.acquireVerifiedFocus( + bundleId: targetAppBundleId, + appName: targetAppName, + maxRetries: maxRetries + ) - log.warning("Frontmost guard (name): frontmost=\(frontmostName), target=\(targetName). No bundle ID for activation retry.") - return "Action blocked: frontmost app is '\(frontmostName)' but target is '\(targetName)'. Please switch to the target app first." + switch result { + case .success: + return nil + case .targetNotRunning: + return "FOCUS_ACQUIRE_FAILED: Target app '\(targetAppName ?? targetAppBundleId ?? "unknown")' is not running." + case .failed(let reason): + return "FOCUS_ACQUIRE_FAILED: \(reason)" } - - return nil } // MARK: - Observation Builder diff --git a/clients/macos/vellum-assistantTests/FocusReliabilityTests.swift b/clients/macos/vellum-assistantTests/FocusReliabilityTests.swift new file mode 100644 index 00000000000..23f3ed68ad2 --- /dev/null +++ b/clients/macos/vellum-assistantTests/FocusReliabilityTests.swift @@ -0,0 +1,254 @@ +import XCTest +@testable import VellumAssistantLib + +// MARK: - AXElementRegistry Tests + +final class AXElementRegistryTests: XCTestCase { + + func testRegisterAndResolve() { + let registry = AXElementRegistry() + let pid = ProcessInfo.processInfo.processIdentifier + let element = AXUIElementCreateApplication(pid) + + registry.register(elementId: 42, element: element) + + XCTAssertNotNil(registry.resolve(elementId: 42)) + XCTAssertNil(registry.resolve(elementId: 99), "Unregistered ID should return nil") + XCTAssertEqual(registry.count, 1) + } + + func testClearRemovesAllEntries() { + let registry = AXElementRegistry() + let pid = ProcessInfo.processInfo.processIdentifier + let element = AXUIElementCreateApplication(pid) + + registry.register(elementId: 1, element: element) + registry.register(elementId: 2, element: element) + XCTAssertEqual(registry.count, 2) + + registry.clear() + XCTAssertEqual(registry.count, 0) + XCTAssertNil(registry.resolve(elementId: 1)) + XCTAssertNil(registry.resolve(elementId: 2)) + } + + func testResolveStaleIdReturnsNil() { + let registry = AXElementRegistry() + let pid = ProcessInfo.processInfo.processIdentifier + let element = AXUIElementCreateApplication(pid) + + registry.register(elementId: 5, element: element) + registry.clear() + registry.register(elementId: 10, element: element) + + // Old ID should be gone + XCTAssertNil(registry.resolve(elementId: 5)) + // New ID should work + XCTAssertNotNil(registry.resolve(elementId: 10)) + } +} + +// MARK: - FocusManager Tests + +final class FocusManagerTests: XCTestCase { + + @MainActor + func testNoTargetConstraint_alwaysSucceeds() async { + let manager = FocusManager() + let result = await manager.acquireVerifiedFocus(bundleId: nil, appName: nil) + // No target = always matches whatever is frontmost + if case .success = result { + // expected + } else { + XCTFail("Expected success when no target is specified, got \(result)") + } + } + + @MainActor + func testNonexistentApp_returnsNotRunning() async { + let manager = FocusManager() + let result = await manager.acquireVerifiedFocus( + bundleId: "com.nonexistent.bogusapp.doesnotexist", + appName: "BogusAppThatDoesNotExist" + ) + if case .targetNotRunning = result { + // expected + } else { + XCTFail("Expected targetNotRunning for nonexistent app, got \(result)") + } + } + + @MainActor + func testEmptyBundleId_fallsToName() async { + let manager = FocusManager() + // Empty bundle ID should be treated as "no bundle ID" + let result = await manager.acquireVerifiedFocus( + bundleId: "", + appName: "NonexistentApp999" + ) + if case .targetNotRunning = result { + // expected — app name doesn't match any running app + } else { + XCTFail("Expected targetNotRunning for empty bundleId + unknown name, got \(result)") + } + } +} + +// MARK: - AXActionExecutor Tests + +final class AXActionExecutorTests: XCTestCase { + + @MainActor + func testClickUnregisteredElement_returnsFallback() { + let registry = AXElementRegistry() + let executor = AXActionExecutor(elementRegistry: registry) + + let result = executor.click(elementId: 999) + if case .fallback(let reason) = result { + XCTAssertTrue(reason.contains("not in registry")) + } else { + XCTFail("Expected fallback for unregistered element") + } + } + + @MainActor + func testTypeUnregisteredElement_returnsFallback() { + let registry = AXElementRegistry() + let executor = AXActionExecutor(elementRegistry: registry) + + let result = executor.type(elementId: 999, text: "hello") + if case .fallback(let reason) = result { + XCTAssertTrue(reason.contains("not in registry")) + } else { + XCTFail("Expected fallback for unregistered element") + } + } + + @MainActor + func testFocusUnregisteredElement_returnsFallback() { + let registry = AXElementRegistry() + let executor = AXActionExecutor(elementRegistry: registry) + + let result = executor.focus(elementId: 999) + if case .fallback(let reason) = result { + XCTAssertTrue(reason.contains("not in registry")) + } else { + XCTFail("Expected fallback for unregistered element") + } + } +} + +// MARK: - ActionTargetMode Tests + +final class ActionTargetModeTests: XCTestCase { + + func testTargetModeAX_whenOnlyElementId() { + let action = AgentAction( + type: .click, + reasoning: "click button", + resolvedFromElementId: 5 + ) + XCTAssertEqual(action.targetMode, .ax) + } + + func testTargetModeVision_whenOnlyCoordinates() { + let action = AgentAction( + type: .click, + reasoning: "click button", + x: 100, + y: 200 + ) + XCTAssertEqual(action.targetMode, .vision) + } + + func testTargetModeMixed_whenBothElementIdAndCoordinates() { + let action = AgentAction( + type: .click, + reasoning: "click button", + x: 100, + y: 200, + resolvedFromElementId: 5 + ) + XCTAssertEqual(action.targetMode, .mixed) + } + + func testTargetModeUnknown_whenNeitherElementIdNorCoordinates() { + let action = AgentAction( + type: .type, + reasoning: "type text", + text: "hello" + ) + XCTAssertEqual(action.targetMode, .unknown) + } + + func testDragTargetModeMixed_whenBothSourceAndDestinationElementIds() { + let action = AgentAction( + type: .drag, + reasoning: "drag", + x: 10, y: 20, + toX: 30, toY: 40, + resolvedFromElementId: 1, + resolvedToElementId: 2 + ) + XCTAssertEqual(action.targetMode, .mixed) + } +} + +// MARK: - ExecutorError.focusAcquireFailed Tests + +final class ExecutorFocusAcquireFailedTests: XCTestCase { + + func testFocusAcquireFailed_errorDescription() { + let error = ExecutorError.focusAcquireFailed("Expected 'Safari' but frontmost is 'Finder'") + XCTAssertEqual(error.errorDescription, "FOCUS_ACQUIRE_FAILED: Expected 'Safari' but frontmost is 'Finder'") + } + + func testFocusAcquireFailed_isLocalizedError() { + let error: Error = ExecutorError.focusAcquireFailed("test reason") + XCTAssertTrue(error.localizedDescription.contains("FOCUS_ACQUIRE_FAILED")) + } +} + +// MARK: - Strict QA Session Focus Tests + +final class StrictQAFocusTests: XCTestCase { + + /// In strict QA, post-action focus drift should fail the session (not just warn). + @MainActor + func testStrictQA_postActionFocusDrift_failsRun() async { + // This test verifies the contract: when strictVisualQa is true and + // the target app is not frontmost after an action, the session transitions + // to .failed state rather than appending an error and continuing. + // + // We can't easily mock FocusManager (it's internal to Session), but we + // verify the error message format matches the strict failure path. + let error = ExecutorError.focusAcquireFailed("post-action drift — Could not activate 'TestApp' after 1 attempts.") + XCTAssertTrue(error.localizedDescription.contains("FOCUS_ACQUIRE_FAILED")) + XCTAssertTrue(error.localizedDescription.contains("post-action drift")) + } + + /// Non-strict sessions should NOT hard-fail on the same conditions. + func testNonStrict_focusDrift_noHardFail() { + // In non-strict mode, focus drift appends to executionError but does not + // terminate the session. This is a design invariant test. + let action = AgentAction( + type: .click, + reasoning: "click button", + x: 100, + y: 200 + ) + // Non-strict actions should not have requireExactAppMatch set + XCTAssertFalse(action.requireExactAppMatch) + } + + /// Strict QA actions should set requireExactAppMatch. + func testStrictQA_setsRequireExactAppMatch() { + let action = AgentAction( + type: .openApp, + reasoning: "open app", + appName: "Safari", + requireExactAppMatch: true + ) + XCTAssertTrue(action.requireExactAppMatch) + } +} diff --git a/clients/macos/vellum-assistantTests/SessionTests.swift b/clients/macos/vellum-assistantTests/SessionTests.swift index 350c49e7a3d..e718a9ef758 100644 --- a/clients/macos/vellum-assistantTests/SessionTests.swift +++ b/clients/macos/vellum-assistantTests/SessionTests.swift @@ -55,6 +55,7 @@ final class MockDaemonClient: DaemonClientProtocol { final class MockAccessibilityTreeEnumerator: AccessibilityTreeProviding { var result: (elements: [AXElement], windowTitle: String, appName: String, pid: pid_t)? var secondaryWindowCallCount = 0 + let elementRegistry: AXElementRegistry? = nil init(result: (elements: [AXElement], windowTitle: String, appName: String)? = nil) { if let r = result { From ea26e3eef48465f50ca48af7651715629dd5a70f Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Tue, 24 Feb 2026 12:43:55 -0500 Subject: [PATCH 67/72] feat(skills): add screen-recording awareness skill (#8011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a bundled screen-recording SKILL.md that teaches the model about screen recording capabilities, lifecycle, retention, and limitations. Awareness-only — no tools, just contextual knowledge for the model. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../bundled-skills/screen-recording/SKILL.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 assistant/src/config/bundled-skills/screen-recording/SKILL.md diff --git a/assistant/src/config/bundled-skills/screen-recording/SKILL.md b/assistant/src/config/bundled-skills/screen-recording/SKILL.md new file mode 100644 index 00000000000..aed4e7a7b24 --- /dev/null +++ b/assistant/src/config/bundled-skills/screen-recording/SKILL.md @@ -0,0 +1,62 @@ +--- +name: "Screen Recording" +description: "Capture screen recordings during computer-use sessions. Records the display or a specific window as H.264 MP4 video with optional audio." +user-invocable: false +disable-model-invocation: false +metadata: + vellum: + emoji: "🎥" + os: ["darwin"] +--- + +# Screen Recording + +You have access to a screen recording capability that captures what happens on the user's Mac during computer-use sessions. + +## How It Works + +- **Automatic in QA mode**: When a QA/test session starts, recording begins automatically before any destructive actions (clicks, typing, etc.) +- **Can be requested explicitly**: Sessions can be configured with `requiresRecording: true` to enable recording outside of QA mode +- **Recording gate**: When recording is required, destructive actions are blocked until the first video frame is confirmed captured + +## Recording Details + +- **Format**: H.264 MP4 video at 30 fps +- **Resolution**: 1920×1080 +- **Bitrate**: 4 Mbps video, 128 kbps AAC audio (when audio is enabled) +- **Capture scope**: Either the full display or a specific window +- **Storage**: Files are saved to `~/Library/Application Support/vellum-assistant/recordings/` +- **Naming**: `qa-recording-{timestamp}.mp4` + +## Health Checks + +A first-frame handshake verifies the capture pipeline is healthy within 5 seconds of starting. If no frames arrive: +- **Required recording**: The session fails immediately with a clear error +- **Optional recording**: A warning is shown but the session continues + +## After Recording + +When a recording completes: +1. The video file is saved to disk +2. A file-backed attachment is created (metadata in DB, file stays on disk) +3. The attachment is linked to the originating chat message +4. The video appears inline in the conversation for playback + +## Retention + +Recordings have an expiration timestamp (default: 7 days, configurable). An automatic cleanup worker runs periodically (every 6 hours) and deletes expired recording files from disk. + +## Limitations + +- Requires Screen Recording permission in System Settings > Privacy & Security +- Only available on macOS +- One recording at a time per session +- Recording must be stopped explicitly (or stops when the session ends) +- Large recordings consume disk space until cleanup runs + +## When to Mention Recording + +- Tell users their QA session is being recorded when relevant +- Offer to analyze recordings using the media-processing skill (keyframe extraction, event detection) +- Mention retention period if users ask about storage or cleanup +- If recording fails, explain the specific error (permission denied, no display found, etc.) From 3d1527e4806cec173fda6e7a421133d88d09f799 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Tue, 24 Feb 2026 12:44:16 -0500 Subject: [PATCH 68/72] feat(skills): add qa-testing orchestration skill (#8015) Add a bundled qa-testing skill that composes screen-recording and media-processing via includes. This is an awareness-only skill (no TOOLS.json) that teaches the model about QA workflows, strict focus management, recording lifecycle, and post-session video analysis. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- .../config/bundled-skills/qa-testing/SKILL.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 assistant/src/config/bundled-skills/qa-testing/SKILL.md diff --git a/assistant/src/config/bundled-skills/qa-testing/SKILL.md b/assistant/src/config/bundled-skills/qa-testing/SKILL.md new file mode 100644 index 00000000000..4b338416c56 --- /dev/null +++ b/assistant/src/config/bundled-skills/qa-testing/SKILL.md @@ -0,0 +1,93 @@ +--- +name: "QA Testing" +description: "Run QA and testing workflows with automatic screen recording, strict focus management, and post-session video analysis." +user-invocable: false +disable-model-invocation: false +includes: ["screen-recording", "media-processing"] +metadata: {"vellum": {"emoji": "🧪", "os": ["darwin"]}} +--- + +# QA Testing + +This skill orchestrates QA and testing workflows by composing screen recording and media analysis capabilities. + +## How QA Mode Works + +QA mode is activated automatically when the user's message indicates a testing intent — phrases like "test the login flow", "QA the checkout", "verify the form works", etc. You do NOT need to explicitly activate it. + +### What happens in QA mode: + +1. **Screen recording starts automatically** before any destructive actions +2. **Strict focus management** ensures the target app stays frontmost throughout +3. **Recording gate** blocks clicks/typing until the first video frame is captured +4. **Post-action focus drift** is terminal — if the target app loses focus, the session fails immediately +5. **On completion**, the recording is attached to the chat as a playable video + +### QA Latch + +Once QA mode is activated for a conversation, it "latches" — subsequent tasks in the same thread inherit QA mode automatically. The user can opt out with phrases like "stop QA mode" or "disable recording." + +## Strict Visual QA + +When a target app is specified (bundle ID or app name), strict visual QA activates: + +- **Pre-action**: Focus is verified before every destructive action +- **Post-action**: Focus is re-verified after every action +- **open_app verification**: After opening an app, FocusManager confirms it's actually frontmost +- **Failure is terminal**: Any focus drift immediately fails the session (no retry, no continue) + +This ensures the recording captures exactly what happened in the target app, with no accidental interactions in other apps. + +## Post-Session Analysis + +After a QA session completes, you can offer to analyze the recording: + +1. **Ingest** the recording using the media-processing skill's `ingest_media` tool +2. **Extract keyframes** to see what happened at each step +3. **Analyze keyframes** to detect UI changes, errors, or unexpected states +4. **Detect events** to find specific moments (button clicks, form submissions, errors) +5. **Generate clips** of interesting segments for sharing + +### Example follow-up offers: +- "Would you like me to analyze the recording to identify any issues?" +- "I can extract keyframes from the recording to create a visual summary of the test." +- "Want me to check if any error dialogs appeared during the test?" + +## Target App Scoping + +QA sessions are scoped to a specific app. The system: +- Injects the target app as a soft constraint in the system prompt +- Does NOT automatically open the app (you decide based on what's on screen) +- Blocks cross-app actions via the proxy resolver +- Uses FocusManager with multi-strategy activation (unhide, NSRunningApplication.activate, AX window raise) + +## Recording Lifecycle in QA + +``` +Session Start + -> ScreenRecorder.startRecording() + -> Wait for first frame (5s timeout) + -> If required and no frames: FAIL immediately + -> If optional and no frames: warn and continue + +During Session + -> Recording runs continuously + -> Focus verified before/after each action + +Session End + -> ScreenRecorder.stopRecording() -> RecordingResult + -> createFileBackedAttachment() stores metadata + -> linkAttachmentToMessage() ties video to chat + -> Video appears inline for playback + -> Cleanup worker deletes after retention period (default: 7 days) +``` + +## Error Handling + +When recording or focus issues occur, communicate specific errors to the user: +- "Screen Recording permission is not granted" — direct them to System Settings +- "First frame not received within 5 seconds" — capture pipeline issue +- "Target app lost focus during testing" — another app stole focus +- "Could not activate [app] after N attempts" — app may be unresponsive + +Do NOT use generic error messages. Always surface the specific failure reason. From 1b0d9d2680e0ecbc446349009c224b0e686789f7 Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Tue, 24 Feb 2026 12:44:31 -0500 Subject: [PATCH 69/72] fix(macos): decouple ScreenRecorder instantiation from qaMode (#8018) Instantiate ScreenRecorder when either qaMode or requiresRecording is true, allowing screen recording to be used independently of QA mode. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 1e4cefc890a..74415fb558d 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -166,7 +166,7 @@ extension AppDelegate { sessionId: routed.sessionId, skipSessionCreate: true, notificationService: self.services.activityNotificationService, - screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, + screenRecorder: (routed.qaMode == true || routed.requiresRecording == true) ? ScreenRecorder() : nil, reportToSessionId: routed.reportToSessionId, qaMode: routed.qaMode ?? false, retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, @@ -342,7 +342,7 @@ extension AppDelegate { sessionId: routed.sessionId, skipSessionCreate: true, notificationService: self.services.activityNotificationService, - screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, + screenRecorder: (routed.qaMode == true || routed.requiresRecording == true) ? ScreenRecorder() : nil, reportToSessionId: routed.reportToSessionId, qaMode: routed.qaMode ?? false, retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, From 3026881d825d84260138791c246d5de85713ff9f Mon Sep 17 00:00:00 2001 From: Jason Zhou Date: Tue, 24 Feb 2026 12:44:45 -0500 Subject: [PATCH 70/72] feat: allow requiresRecording to be set independently of QA intent (#8021) Add optional `requiresRecording` field to `TaskSubmit` IPC message type so callers can explicitly request screen recording without triggering QA mode. The daemon routing in misc.ts now uses `msg.requiresRecording` as an override, falling back to the existing QA-based computation when the field is not set. Co-authored-by: Vellum Assistant Co-authored-by: Claude Opus 4.6 --- assistant/src/daemon/handlers/misc.ts | 4 ++-- assistant/src/daemon/ipc-contract/computer-use.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index 5be41331977..4b1a1c62eb6 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -87,8 +87,8 @@ export async function handleTaskSubmit( } const config = getConfig(); - // Determine whether recording is required: (QA intent or active latch) + config flag - const requiresRecording = (isQa || (qaLatchActive && !isOptOut)) && config.qaRecording.enforceStartBeforeActions; + // Determine whether recording is required: explicit flag on the message, or (QA intent or active latch) + config flag + const requiresRecording = msg.requiresRecording ?? ((isQa || (qaLatchActive && !isOptOut)) && config.qaRecording.enforceStartBeforeActions); rlog.info({ interactionType, diff --git a/assistant/src/daemon/ipc-contract/computer-use.ts b/assistant/src/daemon/ipc-contract/computer-use.ts index e96a5f8e685..6a1a9edd94b 100644 --- a/assistant/src/daemon/ipc-contract/computer-use.ts +++ b/assistant/src/daemon/ipc-contract/computer-use.ts @@ -100,6 +100,8 @@ export interface TaskSubmit { screenHeight: number; attachments?: UserMessageAttachment[]; source?: 'voice' | 'text'; + /** When set, overrides the QA-based requiresRecording computation. */ + requiresRecording?: boolean; } export interface RideShotgunStart { From 9151823f5d1233fbf26331edaf001ec3673f261b Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Tue, 24 Feb 2026 13:42:22 -0500 Subject: [PATCH 71/72] fix: always forward RUNTIME_HTTP_PORT to daemon for video playback - Add RUNTIME_HTTP_PORT to daemon env forwarding list in cli/local.ts - Remove localHttpEnabled feature flag gate on RUNTIME_HTTP_PORT in AppDelegate.swift - Always include RUNTIME_HTTP_PORT with default 7821 in AssistantCli.swift env Co-Authored-By: Claude --- cli/src/lib/local.ts | 1 + clients/macos/vellum-assistant/App/AppDelegate.swift | 11 +++++------ .../macos/vellum-assistant/App/AssistantCli.swift | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/src/lib/local.ts b/cli/src/lib/local.ts index 3d3e422aeac..eb60b4807d6 100644 --- a/cli/src/lib/local.ts +++ b/cli/src/lib/local.ts @@ -193,6 +193,7 @@ export async function startLocalDaemon(): Promise { for (const key of [ "ANTHROPIC_API_KEY", "BASE_DATA_DIR", + "RUNTIME_HTTP_PORT", "VELLUM_DAEMON_TCP_PORT", "VELLUM_DAEMON_TCP_HOST", "VELLUM_DAEMON_SOCKET", diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 5021c71aacb..5822a8806ce 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -572,12 +572,11 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { let assistant = loadAssistantFromLockfile() - // Ensure the daemon starts its runtime HTTP server so the app - // can communicate over HTTP instead of IPC. - if FeatureFlagManager.shared.isEnabled(.localHttpEnabled) { - if ProcessInfo.processInfo.environment["RUNTIME_HTTP_PORT"] == nil { - setenv("RUNTIME_HTTP_PORT", "7821", 0) - } + // Ensure the daemon always starts its runtime HTTP server. + // Required for file-backed attachment content streaming (video playback), + // and optionally used for HTTP-based IPC transport when localHttpEnabled is on. + if ProcessInfo.processInfo.environment["RUNTIME_HTTP_PORT"] == nil { + setenv("RUNTIME_HTTP_PORT", "7821", 0) } configureDaemonTransport(for: assistant) diff --git a/clients/macos/vellum-assistant/App/AssistantCli.swift b/clients/macos/vellum-assistant/App/AssistantCli.swift index 197e44e8b2e..ac8b64294a0 100644 --- a/clients/macos/vellum-assistant/App/AssistantCli.swift +++ b/clients/macos/vellum-assistant/App/AssistantCli.swift @@ -593,6 +593,12 @@ final class AssistantCli { "HOME": NSHomeDirectory(), "PATH": fullEnv["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", "VELLUM_DESKTOP_APP": "1", + // Always start the HTTP server for file-backed attachment content + // streaming (video playback). ProcessInfo.processInfo.environment is + // cached at launch, so read via getenv() which reflects setenv() calls. + "RUNTIME_HTTP_PORT": fullEnv["RUNTIME_HTTP_PORT"] + ?? getenv("RUNTIME_HTTP_PORT").flatMap({ String(cString: $0) }) + ?? "7821", ] // Forward optional config vars the CLI or daemon may need for key in ["ANTHROPIC_API_KEY", "BASE_DATA_DIR", "VELLUM_DEBUG", @@ -602,12 +608,6 @@ final class AssistantCli { env[key] = val } } - // Forward RUNTIME_HTTP_PORT only when the localHttpEnabled flag - // is active, so the daemon doesn't start its HTTP server by default. - if FeatureFlagManager.shared.isEnabled(.localHttpEnabled), - let port = fullEnv["RUNTIME_HTTP_PORT"] ?? getenv("RUNTIME_HTTP_PORT").flatMap({ String(cString: $0) }) { - env["RUNTIME_HTTP_PORT"] = port - } proc.environment = env try proc.run() From 6c53b2582401681f8c5346749ed2ddda9e92ca72 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Tue, 24 Feb 2026 17:45:06 -0500 Subject: [PATCH 72/72] feat: standalone screen recording and self-targeted CU sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add recording-intent detector (recording-intent.ts) to trigger recording without requiring QA intent — "record my screen" now works standalone - Route recording-intent tasks to computer_use interaction type - Decouple reportToSessionId from qaMode so video attaches back to chat for both QA and standalone recording sessions - Only apply strictVisualQa guardrails when actually in QA mode - Add allowSelfEnumeration to AccessibilityTreeProviding so CU sessions targeting Vellum itself enumerate the correct window - Auto-detect self-targeted sessions in Session.swift init and configure the enumerator accordingly - Add self-target focus drift detection — warn the agent when frontmost app doesn't match the self-targeted session - Rename finalizeQARecording → finalizeRecording and use shouldFinalizeRecording computed property for clarity - Add conversationId to TaskSubmit IPC contract - Add channel_readiness IPC message types and snapshot tests - Update IPC generated Swift types and snapshot expectations Co-Authored-By: Claude --- .../__snapshots__/ipc-snapshot.test.ts.snap | 34 ++++- assistant/src/__tests__/ipc-snapshot.test.ts | 20 +++ .../src/__tests__/recording-intent.test.ts | 140 +++++++++++++++++ assistant/src/daemon/handlers/misc.ts | 24 ++- .../src/daemon/ipc-contract/computer-use.ts | 2 + assistant/src/daemon/recording-intent.ts | 23 +++ .../ComputerUse/AccessibilityTree.swift | 12 +- .../ComputerUse/Session.swift | 143 ++++++++++++------ .../vellum-assistantTests/SessionTests.swift | 63 ++++++++ .../IPC/Generated/IPCContractGenerated.swift | 11 +- 10 files changed, 409 insertions(+), 63 deletions(-) create mode 100644 assistant/src/__tests__/recording-intent.test.ts create mode 100644 assistant/src/daemon/recording-intent.ts diff --git a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap index 7a5586eb04f..471f3f1fdd7 100644 --- a/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +++ b/assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap @@ -37,7 +37,7 @@ exports[`IPC message snapshots ClientMessage types session_create serializes to "threadType": "standard", "title": "New session", "transport": { - "channelId": "desktop", + "channelId": "macos", "hints": [ "dashboard-capable", ], @@ -222,6 +222,7 @@ exports[`IPC message snapshots ClientMessage types watch_observation serializes exports[`IPC message snapshots ClientMessage types task_submit serializes to expected JSON 1`] = ` { + "conversationId": "conv-001", "screenHeight": 1080, "screenWidth": 1920, "task": "Open Safari and search for weather", @@ -643,6 +644,14 @@ exports[`IPC message snapshots ClientMessage types twilio_config serializes to e } `; +exports[`IPC message snapshots ClientMessage types channel_readiness serializes to expected JSON 1`] = ` +{ + "action": "get", + "channel": "telegram", + "type": "channel_readiness", +} +`; + exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = ` { "action": "create_challenge", @@ -2067,6 +2076,29 @@ exports[`IPC message snapshots ServerMessage types twilio_config_response serial } `; +exports[`IPC message snapshots ServerMessage types channel_readiness_response serializes to expected JSON 1`] = ` +{ + "snapshots": [ + { + "channel": "telegram", + "checkedAt": 1700000000, + "localChecks": [ + { + "message": "Bot token configured", + "name": "bot_token", + "passed": true, + }, + ], + "ready": true, + "reasons": [], + "stale": false, + }, + ], + "success": true, + "type": "channel_readiness_response", +} +`; + exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = ` { "instruction": "Send this code to the Telegram bot", diff --git a/assistant/src/__tests__/ipc-snapshot.test.ts b/assistant/src/__tests__/ipc-snapshot.test.ts index a1a02ab25de..da8978d8dc1 100644 --- a/assistant/src/__tests__/ipc-snapshot.test.ts +++ b/assistant/src/__tests__/ipc-snapshot.test.ts @@ -162,6 +162,7 @@ const clientMessages: Record = { task: 'Open Safari and search for weather', screenWidth: 1920, screenHeight: 1080, + conversationId: 'conv-001', }, ui_surface_action: { type: 'ui_surface_action', @@ -409,6 +410,11 @@ const clientMessages: Record = { type: 'twilio_config', action: 'get', }, + channel_readiness: { + type: 'channel_readiness', + action: 'get', + channel: 'telegram', + }, guardian_verification: { type: 'guardian_verification', action: 'create_challenge', @@ -1314,6 +1320,20 @@ const serverMessages: Record = { hasCredentials: true, phoneNumber: '+15551234567', }, + channel_readiness_response: { + type: 'channel_readiness_response', + success: true, + snapshots: [ + { + channel: 'telegram', + ready: true, + checkedAt: 1700000000, + stale: false, + reasons: [], + localChecks: [{ name: 'bot_token', passed: true, message: 'Bot token configured' }], + }, + ], + }, guardian_verification_response: { type: 'guardian_verification_response', success: true, diff --git a/assistant/src/__tests__/recording-intent.test.ts b/assistant/src/__tests__/recording-intent.test.ts new file mode 100644 index 00000000000..a7cdf43bea3 --- /dev/null +++ b/assistant/src/__tests__/recording-intent.test.ts @@ -0,0 +1,140 @@ +import { describe, test, expect } from 'bun:test'; +import { detectRecordingIntent } from '../daemon/recording-intent.js'; +import { detectQaIntent } from '../daemon/qa-intent.js'; + +describe('detectRecordingIntent', () => { + // ── Positive cases ────────────────────────────────────────────────────── + const positives = [ + 'record my screen', + 'record the screen', + 'record screen', + 'Record my display', + 'record my desktop', + 'record my session', + 'screen record this', + 'screenrecord while I work', + 'capture my screen', + 'capture the display', + 'capture my desktop', + 'record this', + 'record while I work', + 'record what I do', + 'record me doing this', + 'start recording', + 'record a video', + 'record video of my workflow', + 'video record this session', + 'make a recording', + 'take a recording', + 'take a screen recording', + ]; + + for (const input of positives) { + test(`detects: "${input}"`, () => { + expect(detectRecordingIntent(input)).toBe(true); + }); + } + + // ── Negative cases ────────────────────────────────────────────────────── + const negatives = [ + 'open Safari', + 'check my email', + 'write a function', + 'what is the weather', + 'record in the database', // "record" without screen/display/etc. + 'help me with this task', + 'open the recording app', // "recording" as noun, not a request + ]; + + for (const input of negatives) { + test(`does not detect: "${input}"`, () => { + expect(detectRecordingIntent(input)).toBe(false); + }); + } + + // ── Mixed QA + recording ──────────────────────────────────────────────── + // These prompts contain both QA intent AND recording intent. + // Both detectors should fire independently. + describe('mixed QA + recording prompts', () => { + const mixedPrompts = [ + 'test this behavior and record the screen', + 'QA the login flow and record my screen', + 'verify the signup and start recording', + 'test the app — record video of the session', + ]; + + for (const input of mixedPrompts) { + test(`"${input}" triggers both QA and recording intent`, () => { + expect(detectQaIntent(input)).toBe(true); + expect(detectRecordingIntent(input)).toBe(true); + }); + } + }); +}); + +// ── Routing integration tests ───────────────────────────────────────────── +// These verify the requiresRecording computation logic from misc.ts +// without needing to spin up the full handler. +describe('requiresRecording computation', () => { + // Mirrors the logic in handleTaskSubmit: + // const requiresRecording = msg.requiresRecording + // ?? (isRecordingRequested || (effectiveQa && config.qaRecording.enforceStartBeforeActions)); + function computeRequiresRecording(opts: { + msgOverride?: boolean; + isRecordingRequested: boolean; + effectiveQa: boolean; + enforceStartBeforeActions: boolean; + }): boolean { + return opts.msgOverride + ?? (opts.isRecordingRequested || (opts.effectiveQa && opts.enforceStartBeforeActions)); + } + + test('standalone recording request → requiresRecording = true', () => { + expect(computeRequiresRecording({ + isRecordingRequested: true, + effectiveQa: false, + enforceStartBeforeActions: false, + })).toBe(true); + }); + + test('QA intent + enforceStartBeforeActions → requiresRecording = true', () => { + expect(computeRequiresRecording({ + isRecordingRequested: false, + effectiveQa: true, + enforceStartBeforeActions: true, + })).toBe(true); + }); + + test('QA intent without enforceStartBeforeActions → requiresRecording = false', () => { + expect(computeRequiresRecording({ + isRecordingRequested: false, + effectiveQa: true, + enforceStartBeforeActions: false, + })).toBe(false); + }); + + test('mixed QA + recording → requiresRecording = true regardless of config', () => { + expect(computeRequiresRecording({ + isRecordingRequested: true, + effectiveQa: true, + enforceStartBeforeActions: false, + })).toBe(true); + }); + + test('explicit msg.requiresRecording overrides computation', () => { + expect(computeRequiresRecording({ + msgOverride: false, + isRecordingRequested: true, + effectiveQa: true, + enforceStartBeforeActions: true, + })).toBe(false); + }); + + test('no intent, no QA, no override → requiresRecording = false', () => { + expect(computeRequiresRecording({ + isRecordingRequested: false, + effectiveQa: false, + enforceStartBeforeActions: true, + })).toBe(false); + }); +}); diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index 4b1a1c62eb6..8a7b707907b 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -21,6 +21,7 @@ import type { import { log, wireEscalationHandler, renderHistoryContent, defineHandlers, setQaLatch, clearQaLatch, isQaLatchActive, type HandlerContext } from './shared.js'; import { handleCuSessionCreate } from './computer-use.js'; import { detectQaIntent, detectQaOptOut, shouldRouteQaToComputerUse } from '../qa-intent.js'; +import { detectRecordingIntent } from '../recording-intent.js'; import { resolveComputerUseTargetAppHint } from '../target-app-hints.js'; // ─── Task submit handler ──────────────────────────────────────────────────── @@ -72,9 +73,10 @@ export async function handleTaskSubmit( const isOptOut = detectQaOptOut(msg.task); const qaLatchActive = isQaLatchActive(msg.conversationId); const forceQaComputerUse = shouldRouteQaToComputerUse(msg.task); + const isRecordingRequested = detectRecordingIntent(msg.task); const interactionType = slashCandidate.kind === 'candidate' ? 'text_qa' as const - : forceQaComputerUse + : (forceQaComputerUse || isRecordingRequested) ? 'computer_use' as const : await classifyInteraction(msg.task, msg.source); @@ -87,14 +89,17 @@ export async function handleTaskSubmit( } const config = getConfig(); - // Determine whether recording is required: explicit flag on the message, or (QA intent or active latch) + config flag - const requiresRecording = msg.requiresRecording ?? ((isQa || (qaLatchActive && !isOptOut)) && config.qaRecording.enforceStartBeforeActions); + const effectiveQa = isQa || (qaLatchActive && !isOptOut); + // Recording required when: explicit flag, standalone recording request, or QA intent + config flag + const requiresRecording = msg.requiresRecording + ?? (isRecordingRequested || (effectiveQa && config.qaRecording.enforceStartBeforeActions)); rlog.info({ interactionType, slashBypass: slashCandidate.kind === 'candidate', taskLength: msg.task.length, isQa, + isRecordingRequested, forceQaComputerUse, qaLatchActive, requiresRecording, @@ -104,8 +109,8 @@ export async function handleTaskSubmit( // Create CU session (reuse handleCuSessionCreate logic) const sessionId = uuid(); const targetApp = resolveComputerUseTargetAppHint(msg.task); - const effectiveQa = isQa || (qaLatchActive && !isOptOut); - const strictVisualQa = requiresRecording && !!(targetApp?.bundleId || targetApp?.appName); + // strictVisualQa only for QA mode — generic recording should NOT inherit QA guardrails + const strictVisualQa = effectiveQa && requiresRecording && !!(targetApp?.bundleId || targetApp?.appName); const cuMsg: CuSessionCreate = { type: 'cu_session_create', sessionId, @@ -115,7 +120,9 @@ export async function handleTaskSubmit( attachments: msg.attachments, interactionType: 'computer_use', ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(effectiveQa ? { qaMode: true, reportToSessionId: msg.conversationId } : {}), + ...(effectiveQa ? { qaMode: true } : {}), + // reportToSessionId needed for both QA and recording-only so the video attaches back to chat + ...(requiresRecording || effectiveQa ? { reportToSessionId: msg.conversationId } : {}), ...(requiresRecording ? { requiresRecording: true } : {}), ...(strictVisualQa ? { strictVisualQa: true } : {}), }; @@ -126,8 +133,9 @@ export async function handleTaskSubmit( sessionId, interactionType: 'computer_use', ...(targetApp ? { targetAppName: targetApp.appName, targetAppBundleId: targetApp.bundleId } : {}), - ...(effectiveQa ? { - qaMode: true, + ...(effectiveQa ? { qaMode: true } : {}), + // Recording config for both QA and standalone recording sessions + ...(requiresRecording || effectiveQa ? { reportToSessionId: msg.conversationId, retentionDays: config.qaRecording.defaultRetentionDays, captureScope: config.qaRecording.captureScope, diff --git a/assistant/src/daemon/ipc-contract/computer-use.ts b/assistant/src/daemon/ipc-contract/computer-use.ts index 6a1a9edd94b..c8052492d62 100644 --- a/assistant/src/daemon/ipc-contract/computer-use.ts +++ b/assistant/src/daemon/ipc-contract/computer-use.ts @@ -102,6 +102,8 @@ export interface TaskSubmit { source?: 'voice' | 'text'; /** When set, overrides the QA-based requiresRecording computation. */ requiresRecording?: boolean; + /** Active conversation/thread ID — used for QA latch tracking and reportToSessionId. */ + conversationId?: string; } export interface RideShotgunStart { diff --git a/assistant/src/daemon/recording-intent.ts b/assistant/src/daemon/recording-intent.ts new file mode 100644 index 00000000000..dabe8261869 --- /dev/null +++ b/assistant/src/daemon/recording-intent.ts @@ -0,0 +1,23 @@ +/** + * Detect whether the user is requesting screen recording. + * This is independent of QA intent — a prompt can trigger both. + * The caller (misc.ts) merges recording intent with QA-based recording + * via the `requiresRecording` computation, so no dedup is needed here. + */ +export function detectRecordingIntent(taskText: string): boolean { + const lower = taskText.toLowerCase().trim(); + + const recordingPatterns = [ + /\brecord\s+((my|the|a)\s+)?(screen|display|desktop|session)\b/, + /\bscreen\s*record/, + /\bcapture\s+((my|the|a)\s+)?(screen|display|desktop)\b/, + /\brecord\s+(this|while|what|me)\b/, + /\bstart\s+recording\b/, + /\brecord\s+(a\s+)?video\b/, + /\bvideo\s+record/, + /\bmake\s+a\s+recording\b/, + /\btake\s+a\s+(screen\s+)?recording\b/, + ]; + + return recordingPatterns.some(p => p.test(lower)); +} diff --git a/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift b/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift index 1f2c7528658..fbf4886cb6e 100644 --- a/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift +++ b/clients/macos/vellum-assistant/ComputerUse/AccessibilityTree.swift @@ -30,9 +30,18 @@ protocol AccessibilityTreeProviding { func enumerateSecondaryWindows(excludingPID: pid_t?, maxWindows: Int) -> [WindowInfo] /// Optional element registry — populated during enumeration for AX-first action targeting. var elementRegistry: AXElementRegistry? { get } + /// When true, enumerating Vellum's own window is allowed (for self-targeted CU sessions). + var allowSelfEnumeration: Bool { get } +} + +extension AccessibilityTreeProviding { + var allowSelfEnumeration: Bool { false } } final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { + /// When true, enumerating Vellum's own window is allowed instead of skipping to the previous app. + var allowSelfEnumeration: Bool = false + private var nextId = 1 /// PID of the last successfully enumerated target app, used to resolve the /// correct app when our own window is frontmost. @@ -118,7 +127,8 @@ final class AccessibilityTreeEnumerator: AccessibilityTreeProviding { } // Skip our own app — we want the window behind the overlay - if let myId = myBundleId, frontApp.bundleIdentifier == myId { + // Unless allowSelfEnumeration is set (self-targeted CU sessions) + if let myId = myBundleId, frontApp.bundleIdentifier == myId, !allowSelfEnumeration { log.info("Skipping own app, looking for previous app") return enumeratePreviousApp() } diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index bf9491c70a9..59caf0e5ea7 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -70,7 +70,7 @@ final class ComputerUseSession: ObservableObject { private let skipSessionCreate: Bool private let notificationService: ActivityNotificationServiceProtocol? - /// Screen recorder for QA mode — nil when not in QA mode. + /// Screen recorder — nil when recording is not requested (neither QA nor standalone recording). private let screenRecorder: ScreenRecording? /// Origin chat session ID for result injection (QA workflow). let reportToSessionId: String? @@ -78,9 +78,9 @@ 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"). + /// Capture scope for screen recording (from daemon config, default "display"). let captureScope: String - /// Whether to include audio in QA recording (from daemon config, default false). + /// Whether to include audio in screen recording (from daemon config, default false). let includeAudio: Bool /// When true, recording MUST start before any action; failure aborts the session. let requiresRecording: Bool @@ -119,7 +119,9 @@ final class ComputerUseSession: ObservableObject { private var consecutiveUnchangedSteps = 0 private var currentStepNumber = 0 private var consecutiveFrontmostBlocks = 0 - private var didFinalizeQARecording = false + private var didFinalizeRecording = false + /// Whether this session should finalize recording on completion (has a recorder, regardless of QA mode). + private var shouldFinalizeRecording: Bool { screenRecorder != nil } @Published private(set) var qaRecordingWarningMessage: String? /// Whether screen recording is currently active (for UI indicator). @Published private(set) var isRecordingActive: Bool = false @@ -133,7 +135,7 @@ final class ComputerUseSession: ObservableObject { init( task: String, daemonClient: DaemonClientProtocol, - enumerator: AccessibilityTreeProviding = AccessibilityTreeEnumerator(), + enumerator: AccessibilityTreeProviding? = nil, screenCapture: ScreenCaptureProviding = ScreenCapture(), executor: ActionExecuting = ActionExecutor(), maxSteps: Int = 50, @@ -160,7 +162,28 @@ final class ComputerUseSession: ObservableObject { self.attachments = attachments self.daemonClient = daemonClient self.interactionType = interactionType - self.enumerator = enumerator + + // Auto-configure enumerator: when the session targets Vellum itself, + // allow enumerating our own window instead of skipping to the previous app. + if let enumerator = enumerator { + self.enumerator = enumerator + } else { + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + let isSelfTargeted: Bool + if let bid = targetAppBundleId, !bid.isEmpty { + isSelfTargeted = bid == selfBundleId + } else if let name = targetAppName, !name.isEmpty { + let lower = name.lowercased() + isSelfTargeted = lower == "vellum" || lower == "vellum assistant" + } else { + isSelfTargeted = false + } + let newEnumerator = AccessibilityTreeEnumerator() + if isSelfTargeted { + newEnumerator.allowSelfEnumeration = true + } + self.enumerator = newEnumerator + } self.screenCapture = screenCapture self.executor = executor self.maxSteps = maxSteps @@ -195,7 +218,7 @@ final class ComputerUseSession: ObservableObject { consecutiveUnchangedSteps = 0 currentStepNumber = 0 consecutiveFrontmostBlocks = 0 - didFinalizeQARecording = false + didFinalizeRecording = false state = .running(step: 0, maxSteps: maxSteps, lastAction: "Starting...", reasoning: "") // QA sessions auto-approve low/medium tools from the start. @@ -214,8 +237,8 @@ final class ComputerUseSession: ObservableObject { try? await Task.sleep(nanoseconds: initialDelayMs * 1_000_000) } - // Start screen recording in QA mode - if qaMode, let recorder = screenRecorder { + // Start screen recording when a recorder is available (QA or standalone recording) + if let recorder = screenRecorder { do { var windowID: CGWindowID? var displayID: CGDirectDisplayID? @@ -236,7 +259,7 @@ final class ComputerUseSession: ObservableObject { } } if windowID == nil { - log.warning("QA mode: captureScope is 'window' but no suitable target window found — falling back to display capture") + log.warning("Recording: captureScope is 'window' but no suitable target window found — falling back to display capture") } } else { displayID = CGMainDisplayID() @@ -246,7 +269,7 @@ final class ComputerUseSession: ObservableObject { // Verify capture pipeline is healthy by waiting for first frame let firstFrameReceived = await recorder.waitForFirstFrame(timeoutSeconds: 5.0) if !firstFrameReceived { - log.error("QA mode: first video frame not received within 5 seconds — capture pipeline unhealthy") + log.error("Recording: first video frame not received within 5 seconds — capture pipeline unhealthy") try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: "First frame not received within 5 seconds")) if requiresRecording { state = .failed(reason: "Recording required but capture pipeline failed: no frames received") @@ -254,13 +277,13 @@ final class ComputerUseSession: ObservableObject { logger.finishSession(result: "failed: first frame timeout") cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil - await finalizeQARecording() + await finalizeRecording() return } qaRecordingWarningMessage = "Recording may be incomplete: capture pipeline was slow to start" } - log.info("QA mode: screen recording started for session \(self.id) (scope: \(self.captureScope))") + log.info("Recording: screen recording started for session \(self.id) (scope: \(self.captureScope))") isRecordingActive = true // Notify daemon that recording started successfully try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "started")) @@ -271,7 +294,7 @@ final class ComputerUseSession: ObservableObject { } else { reason = error.localizedDescription } - log.error("QA mode: failed to start screen recording: \(reason)") + log.error("Recording: failed to start screen recording: \(reason)") // Notify daemon of recording failure try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: reason)) if requiresRecording { @@ -281,7 +304,7 @@ final class ComputerUseSession: ObservableObject { logger.finishSession(result: "failed: required recording could not start") cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil - await finalizeQARecording() + await finalizeRecording() return } // Non-fatal for best-effort recording — continue the session without recording @@ -327,8 +350,8 @@ final class ComputerUseSession: ObservableObject { // Disarm cancel safety net — this run is terminating early. cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil - if qaMode { - await finalizeQARecording() + if shouldFinalizeRecording { + await finalizeRecording() } return } @@ -346,8 +369,8 @@ final class ComputerUseSession: ObservableObject { // Disarm cancel safety net — this run is terminating early. cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil - if qaMode { - await finalizeQARecording() + if shouldFinalizeRecording { + await finalizeRecording() } return } @@ -359,11 +382,11 @@ final class ComputerUseSession: ObservableObject { cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil - // Finalize QA recording BEFORE sending abort — the daemon's handleCuSessionAbort + // Finalize recording BEFORE sending abort — the daemon's handleCuSessionAbort // deletes cuSessionMetadata, so cu_session_finalized must arrive first for // summary injection to work. - if qaMode { - await finalizeQARecording() + if shouldFinalizeRecording { + await finalizeRecording() } do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) @@ -458,10 +481,10 @@ final class ComputerUseSession: ObservableObject { cancelSafetyNetTask?.cancel() cancelSafetyNetTask = nil - // Finalize QA recording and send cu_session_finalized + // Finalize recording and send cu_session_finalized // Guard: skip if already finalized (e.g. frontmost guard limit triggered finalization early) - if qaMode && !didFinalizeQARecording { - await finalizeQARecording() + if shouldFinalizeRecording && !didFinalizeRecording { + await finalizeRecording() // Send the abort immediately after finalization for cancelled QA sessions. // This arrives before cancel()'s 2-second safety-net abort fires. @@ -616,10 +639,10 @@ final class ComputerUseSession: ObservableObject { : "Target app could not be activated after repeated attempts.") logger.finishSession(result: "failed: frontmost guard\(strictVisualQa ? " (strict)" : "") — \(guardError)") - // Finalize QA recording BEFORE sending abort — the daemon's handleCuSessionAbort + // Finalize recording BEFORE sending abort — the daemon's handleCuSessionAbort // deletes cuSessionMetadata, so cu_session_finalized must arrive first. - if qaMode { - await finalizeQARecording() + if shouldFinalizeRecording { + await finalizeRecording() } do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) @@ -704,8 +727,8 @@ final class ComputerUseSession: ObservableObject { isCancelled = true state = .failed(reason: "FOCUS_ACQUIRE_FAILED: open_app '\(openedAppName ?? openedAppBundleId ?? "unknown")' — \(reason)") logger.finishSession(result: "failed: open_app focus verification — \(reason)") - if qaMode { - await finalizeQARecording() + if shouldFinalizeRecording { + await finalizeRecording() } do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) @@ -730,8 +753,8 @@ final class ComputerUseSession: ObservableObject { isCancelled = true state = .failed(reason: "FOCUS_ACQUIRE_FAILED: post-action drift — \(reason)") logger.finishSession(result: "failed: post-action focus drift — \(reason)") - if qaMode { - await finalizeQARecording() + if shouldFinalizeRecording { + await finalizeRecording() } do { try daemonClient.send(CuSessionAbortMessage(sessionId: id)) @@ -809,6 +832,7 @@ final class ComputerUseSession: ObservableObject { var axDiffText: String? var secondaryWindowsText: String? var primaryPID: pid_t? + var mergedExecutionError = executionError let stepNumber = currentStepNumber + 1 @@ -835,6 +859,25 @@ final class ComputerUseSession: ObservableObject { } else { isExternalTarget = false } + + // SELF-TARGET DRIFT DETECTION — if this session explicitly targets Vellum + // but the frontmost app is something else, the AX tree may be from the wrong app. + // Surface a warning in executionError so the agent can react (e.g., refocus Vellum). + let isSelfTargeted = !isExternalTarget && (targetAppBundleId != nil || targetAppName != nil) + if isSelfTargeted { + let selfBundleId = Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant" + if let frontApp = NSWorkspace.shared.frontmostApplication, + frontApp.bundleIdentifier != selfBundleId { + let driftMsg = "SELF_TARGET_FOCUS_DRIFT: Session targets Vellum but frontmost app is '\(frontApp.localizedName ?? frontApp.bundleIdentifier ?? "unknown")'. AX tree may be from wrong app." + log.warning("\(driftMsg)") + if let existing = mergedExecutionError { + mergedExecutionError = "\(existing)\n\(driftMsg)" + } else { + mergedExecutionError = driftMsg + } + } + } + if !didChromeAccessibilityCheck, !isExternalTarget, let frontApp = NSWorkspace.shared.frontmostApplication, @@ -1003,7 +1046,7 @@ final class ComputerUseSession: ObservableObject { coordinateOrigin: screenSizePt != nil ? "top_left" : nil, captureDisplayId: screenshotMetadata.map { Double($0.captureDisplayId) }, executionResult: executionResult, - executionError: executionError, + executionError: mergedExecutionError, axTreeBlob: axTreeBlobRef, screenshotBlob: screenshotBlobRef, frontmostAppName: frontmostAppName, @@ -1345,7 +1388,7 @@ final class ComputerUseSession: ObservableObject { .flatMap { $0.toolCalls } } - // MARK: - QA Recording Finalization + // MARK: - Recording Finalization /// Validates that a video file is playable by checking for a video track with non-zero dimensions and duration. private func validateVideoPlayability(at url: URL) async -> Bool { @@ -1353,29 +1396,29 @@ final class ComputerUseSession: ObservableObject { do { let tracks = try await asset.load(.tracks) guard let videoTrack = tracks.first(where: { $0.mediaType == .video }) else { - log.warning("QA mode: salvage video has no video track at \(url.lastPathComponent)") + log.warning("Recording: salvage video has no video track at \(url.lastPathComponent)") return false } let size = try await videoTrack.load(.naturalSize) guard size.width > 0, size.height > 0 else { - log.warning("QA mode: salvage video has zero-size video track at \(url.lastPathComponent)") + log.warning("Recording: salvage video has zero-size video track at \(url.lastPathComponent)") return false } let duration = try await asset.load(.duration) guard CMTimeGetSeconds(duration) > 0 else { - log.warning("QA mode: salvage video has zero duration at \(url.lastPathComponent)") + log.warning("Recording: salvage video has zero duration at \(url.lastPathComponent)") return false } return true } catch { - log.warning("QA mode: salvage video validation failed at \(url.lastPathComponent): \(error.localizedDescription)") + log.warning("Recording: salvage video validation failed at \(url.lastPathComponent): \(error.localizedDescription)") return false } } /// Stops the screen recorder (if active) and sends a `cu_session_finalized` message to the daemon. - private func finalizeQARecording() async { - defer { didFinalizeQARecording = true } + private func finalizeRecording() async { + defer { didFinalizeRecording = true } // Map SessionState to a status string let status: String @@ -1423,7 +1466,7 @@ final class ComputerUseSession: ObservableObject { expiresAt: expiresAtEpoch ) isRecordingActive = false - log.info("QA recording finalized: \(result.fileURL.lastPathComponent) (\(result.sizeBytes) bytes, \(result.durationMs)ms)") + log.info("Recording finalized: \(result.fileURL.lastPathComponent) (\(result.sizeBytes) bytes, \(result.durationMs)ms)") // Notify daemon that recording stopped successfully try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "stopped")) } catch { @@ -1434,7 +1477,7 @@ final class ComputerUseSession: ObservableObject { } else { reason = error.localizedDescription } - log.error("QA mode: failed to stop screen recording: \(reason)") + log.error("Recording: failed to stop screen recording: \(reason)") try? daemonClient.send(CuRecordingStatusMessage(sessionId: id, status: "failed", reason: reason)) if qaRecordingWarningMessage == nil { qaRecordingWarningMessage = "Unable to finalize recording. \(reason)" @@ -1446,7 +1489,7 @@ final class ComputerUseSession: ObservableObject { let attrs = try? FileManager.default.attributesOfItem(atPath: salvageURL.path) let sizeBytes = (attrs?[.size] as? Int) ?? 0 let expiresAtEpoch = Int(Date().addingTimeInterval(Double(retentionDays) * 24 * 3600).timeIntervalSince1970 * 1000) - log.info("QA mode: salvaging partial recording at \(salvageURL.lastPathComponent) (\(sizeBytes) bytes)") + log.info("Recording: salvaging partial recording at \(salvageURL.lastPathComponent) (\(sizeBytes) bytes)") recordingData = IPCCuSessionFinalizedRecording( localPath: salvageURL.path, mimeType: "video/mp4", @@ -1461,14 +1504,14 @@ final class ComputerUseSession: ObservableObject { ) } else if let salvageURL = (recorder as? ScreenRecorder)?.lastRecordingFileURL, FileManager.default.fileExists(atPath: salvageURL.path) { - log.warning("QA mode: salvage recording at \(salvageURL.lastPathComponent) failed playability check — discarding") + log.warning("Recording: salvage recording at \(salvageURL.lastPathComponent) failed playability check — discarding") } } } // If recording was required but no artifact was produced, mark as failure if requiresRecording && recordingData == nil && status != "failed" { - log.error("QA mode: recording was required but no recording artifact exists") + log.error("Recording: recording was required but no recording artifact exists") // Override status to failed — the session cannot be considered successful without a recording let failedStatus = "failed" let failedSummary = "Recording required but no recording artifact produced" @@ -1480,9 +1523,9 @@ final class ComputerUseSession: ObservableObject { stepCount: stepCount, recording: nil )) - log.info("QA mode: sent cu_session_finalized for session \(self.id) (status: \(failedStatus))") + log.info("Recording: sent cu_session_finalized for session \(self.id) (status: \(failedStatus))") } catch { - log.error("QA mode: failed to send cu_session_finalized: \(error.localizedDescription)") + log.error("Recording: failed to send cu_session_finalized: \(error.localizedDescription)") } return } @@ -1496,9 +1539,9 @@ final class ComputerUseSession: ObservableObject { stepCount: stepCount, recording: recordingData )) - log.info("QA mode: sent cu_session_finalized for session \(self.id) (status: \(status))") + log.info("Recording: sent cu_session_finalized for session \(self.id) (status: \(status))") } catch { - log.error("QA mode: failed to send cu_session_finalized: \(error.localizedDescription)") + log.error("Recording: failed to send cu_session_finalized: \(error.localizedDescription)") } } @@ -1521,7 +1564,7 @@ final class ComputerUseSession: ObservableObject { confirmationContinuation = nil pendingToolPermissionPrompt = nil - if qaMode { + if shouldFinalizeRecording { // Deferred abort: give run() a chance to send finalization first, // but guarantee abort eventually fires as a safety net in case // run() never reaches the post-loop block (e.g., throws or gets stuck). diff --git a/clients/macos/vellum-assistantTests/SessionTests.swift b/clients/macos/vellum-assistantTests/SessionTests.swift index e718a9ef758..660ea26e412 100644 --- a/clients/macos/vellum-assistantTests/SessionTests.swift +++ b/clients/macos/vellum-assistantTests/SessionTests.swift @@ -56,6 +56,7 @@ final class MockAccessibilityTreeEnumerator: AccessibilityTreeProviding { var result: (elements: [AXElement], windowTitle: String, appName: String, pid: pid_t)? var secondaryWindowCallCount = 0 let elementRegistry: AXElementRegistry? = nil + var allowSelfEnumeration: Bool = false init(result: (elements: [AXElement], windowTitle: String, appName: String)? = nil) { if let r = result { @@ -1673,6 +1674,68 @@ final class SessionTests: XCTestCase { XCTAssertGreaterThan(inlineLargeJSON.count, 400_000) XCTAssertLessThan(blobLargeJSON.count, 1_000) } + + // MARK: - Self-Enumeration + + @MainActor + func testAllowSelfEnumeration_defaultsFalse() async { + let enumerator = AccessibilityTreeEnumerator() + XCTAssertFalse(enumerator.allowSelfEnumeration, "allowSelfEnumeration should default to false") + } + + @MainActor + func testMockAllowSelfEnumeration_defaultsFalse() async { + let mock = MockAccessibilityTreeEnumerator() + XCTAssertFalse(mock.allowSelfEnumeration, "Mock allowSelfEnumeration should default to false") + } + + @MainActor + func testSelfTargetedSession_includesVellumAXTree() async { + let daemonClient = MockDaemonClient() + let continuation = daemonClient.setupTestStream() + let enumerator = MockAccessibilityTreeEnumerator( + result: (elements: makeTestElements(), windowTitle: "Vellum Window", appName: "Vellum") + ) + // Simulate a self-targeted session — the enumerator should be used as-is + let session = ComputerUseSession( + task: "test self-targeted", + daemonClient: daemonClient, + enumerator: enumerator, + screenCapture: MockScreenCapture(), + executor: MockActionExecutor(), + maxSteps: 50, + initialDelayMs: 0, + adaptiveDelay: false, + targetAppName: "Vellum" + ) + + let runTask = Task { @MainActor in + await session.run() + } + + try? await Task.sleep(nanoseconds: 50_000_000) + + // The observation should contain the AX tree from the enumerator (Vellum's own window) + let obsMessages = daemonClient.sentMessages.compactMap { $0 as? CuObservationMessage } + XCTAssertGreaterThanOrEqual(obsMessages.count, 1, "Should have sent at least one observation") + let firstObs = obsMessages[0] + XCTAssertNotNil(firstObs.axTree, "Self-targeted session should include AX tree") + XCTAssertTrue(firstObs.axTree?.contains("Vellum") == true, "AX tree should reference Vellum app") + + // Clean up + continuation.yield(.cuComplete(makeCompleteMessage( + sessionId: session.id, + summary: "Self-target done", + stepCount: 1 + ))) + await runTask.value + + if case .completed(let summary, _) = session.state { + XCTAssertEqual(summary, "Self-target done") + } else { + XCTFail("Expected completed state, got \(session.state)") + } + } } /// Test elements with 3+ interactive elements (enough for screenshot skip) diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index 71279ad62ec..846d7af0fd4 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -3075,14 +3075,16 @@ public struct IPCSessionListResponseSession: Codable, Sendable { public let source: String? /// Channel binding metadata exposed in session/conversation list APIs. public let channelBinding: IPCChannelBinding? + public let conversationOriginChannel: String? - public init(id: String, title: String, updatedAt: Int, threadType: String? = nil, source: String? = nil, channelBinding: IPCChannelBinding? = nil) { + public init(id: String, title: String, updatedAt: Int, threadType: String? = nil, source: String? = nil, channelBinding: IPCChannelBinding? = nil, conversationOriginChannel: String? = nil) { self.id = id self.title = title self.updatedAt = updatedAt self.threadType = threadType self.source = source self.channelBinding = channelBinding + self.conversationOriginChannel = conversationOriginChannel } } @@ -3901,16 +3903,19 @@ public struct IPCTaskSubmit: Codable, Sendable { public let screenHeight: Int public let attachments: [IPCUserMessageAttachment]? public let source: String? - /// The originating conversation/thread ID, if submitting from a chat context. + /// When set, overrides the QA-based requiresRecording computation. + public let requiresRecording: Bool? + /// Active conversation/thread ID — used for QA latch tracking and reportToSessionId. public let conversationId: String? - public init(type: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]? = nil, source: String? = nil, conversationId: String? = nil) { + public init(type: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCUserMessageAttachment]? = nil, source: String? = nil, requiresRecording: Bool? = nil, conversationId: String? = nil) { self.type = type self.task = task self.screenWidth = screenWidth self.screenHeight = screenHeight self.attachments = attachments self.source = source + self.requiresRecording = requiresRecording self.conversationId = conversationId } }