(workspaceId: string, force = false)
| 3719 | } |
| 3720 | |
| 3721 | async remove(workspaceId: string, force = false): Promise<Result<void>> { |
| 3722 | // Idempotent: if already removing, return success to prevent race conditions |
| 3723 | if (this.removingWorkspaces.has(workspaceId)) { |
| 3724 | return Ok(undefined); |
| 3725 | } |
| 3726 | this.removingWorkspaces.add(workspaceId); |
| 3727 | |
| 3728 | // If this workspace is mid-init, cancel the fire-and-forget init work (postCreateSetup, |
| 3729 | // sync/checkout, .mux/init hook, etc.) so removal doesn't leave orphaned background work. |
| 3730 | const initAbortController = this.initAbortControllers.get(workspaceId); |
| 3731 | if (initAbortController) { |
| 3732 | initAbortController.abort(); |
| 3733 | this.initAbortControllers.delete(workspaceId); |
| 3734 | } |
| 3735 | |
| 3736 | const persistedWorkspace = this.config.findWorkspace(workspaceId); |
| 3737 | |
| 3738 | // Try to remove from runtime (filesystem) |
| 3739 | try { |
| 3740 | if (!force) { |
| 3741 | const config = this.config.loadConfigOrDefault(); |
| 3742 | const taskSettings = normalizeTaskSettings(config.taskSettings); |
| 3743 | if ( |
| 3744 | taskSettings.preserveSubagentsUntilArchive && |
| 3745 | this.taskService?.hasCompletedDescendants?.(workspaceId) |
| 3746 | ) { |
| 3747 | const persistedWorkspaceEntry = findWorkspaceEntry(config, workspaceId); |
| 3748 | const isArchived = |
| 3749 | persistedWorkspaceEntry != null && |
| 3750 | isWorkspaceArchived( |
| 3751 | persistedWorkspaceEntry.workspace.archivedAt, |
| 3752 | persistedWorkspaceEntry.workspace.unarchivedAt |
| 3753 | ); |
| 3754 | |
| 3755 | // Keep the whole parentWorkspaceId chain intact while completed descendants still exist. |
| 3756 | // Unarchived ancestors must be archived first so descendant cleanup can safely walk that lineage. |
| 3757 | if (!isArchived) { |
| 3758 | return Err( |
| 3759 | "This workspace has preserved completed sub-agent workspaces. Archive the workspace first to trigger cleanup, then try removing it." |
| 3760 | ); |
| 3761 | } |
| 3762 | |
| 3763 | // Archived parents can still retain completed descendants while cleanup waits on |
| 3764 | // prerequisites like pending patch artifacts. Keep removal blocked until that cleanup |
| 3765 | // finishes so descendants do not lose the archived ancestor that makes them eligible. |
| 3766 | return Err( |
| 3767 | "This workspace still has completed sub-agent workspaces pending cleanup. Wait for cleanup to finish, or force-remove the workspace." |
| 3768 | ); |
| 3769 | } |
| 3770 | } |
| 3771 | |
| 3772 | // Stop any active stream before deleting metadata/config to avoid tool calls racing with removal. |
| 3773 | // |
| 3774 | // IMPORTANT: AIService forwards "stream-abort" asynchronously after partial cleanup. If we roll up |
| 3775 | // session timing (or delete session files) immediately after stopStream(), we can race the final |
| 3776 | // abort timing write. |
| 3777 | const wasStreaming = this.aiService.isStreaming(workspaceId); |
| 3778 | const streamStoppedEvent: Promise<"abort" | "end" | undefined> | undefined = wasStreaming |
nothing calls this directly
no test coverage detected