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