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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assistant/src/daemon/handlers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────

Expand Down Expand Up @@ -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,
Expand All @@ -82,13 +84,15 @@ export async function handleTaskSubmit(
screenHeight: msg.screenHeight,
attachments: msg.attachments,
interactionType: 'computer_use',
...(isQa ? { qaMode: true } : {}),
};
handleCuSessionCreate(cuMsg, socket, ctx);

ctx.send(socket, {
type: 'task_routed',
sessionId,
interactionType: 'computer_use',
...(isQa ? { qaMode: true } : {}),
});
} else {
// Create text QA session and immediately start processing
Expand Down
6 changes: 6 additions & 0 deletions assistant/src/daemon/handlers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -235,13 +236,16 @@ export function wireEscalationHandler(
}

const cuSessionId = uuid();
const isQa = detectQaIntent(task);
const cuMsg: CuSessionCreate = {
type: 'cu_session_create',
sessionId: cuSessionId,
task,
screenWidth,
screenHeight,
interactionType: 'computer_use',
reportToSessionId: sourceSessionId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate escalation report metadata behind QA finalization support

Setting reportToSessionId on every escalation stores CU metadata for every escalated run, but the macOS client only sends cu_session_finalized when qaMode is true (ComputerUseSession.run), so non-QA escalations never trigger the metadata cleanup path in handleCuSessionFinalized. In long-lived desktop sessions this accumulates stale cuSessionMetadata entries and leaves the new report-linking path effectively incomplete unless QA mode is also enabled.

Useful? React with 👍 / 👎.

...(isQa ? { qaMode: true } : {}),
};
handleCuSessionCreate(cuMsg, currentSocket, ctx);

Expand All @@ -251,6 +255,8 @@ export function wireEscalationHandler(
interactionType: 'computer_use',
task,
escalatedFrom: sourceSessionId,
reportToSessionId: sourceSessionId,
...(isQa ? { qaMode: true } : {}),
});

return true;
Expand Down
4 changes: 4 additions & 0 deletions assistant/src/daemon/ipc-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,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 RideShotgunResult {
Expand Down
21 changes: 21 additions & 0 deletions assistant/src/daemon/qa-intent.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Narrow QA intent matcher for generic “check” commands

The direct matcher marks any task beginning with check as QA intent, so routine computer-use requests like “check my inbox/calendar/slack” are treated as QA/test workflows even when no testing is intended. That broad trigger can unexpectedly flip sessions into QA-specific behavior and pollute QA/reporting paths for normal user tasks.

Useful? React with 👍 / 👎.


// 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));
}
6 changes: 3 additions & 3 deletions clients/macos/vellum-assistant/ComputerUse/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions clients/shared/IPC/Generated/IPCContractGenerated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,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?
/// 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.
Expand Down
Loading