| 2911 | } |
| 2912 | |
| 2913 | async createWorkspaceTurn( |
| 2914 | args: WorkspaceTurnCreateArgs |
| 2915 | ): Promise<Result<WorkspaceTurnCreateResult, string>> { |
| 2916 | const ownerWorkspaceId = coerceNonEmptyString(args.ownerWorkspaceId); |
| 2917 | if (!ownerWorkspaceId) { |
| 2918 | return Err("Task.createWorkspaceTurn: ownerWorkspaceId is required"); |
| 2919 | } |
| 2920 | const prompt = coerceNonEmptyString(args.prompt); |
| 2921 | if (!prompt) { |
| 2922 | return Err("Task.createWorkspaceTurn: prompt is required"); |
| 2923 | } |
| 2924 | const title = coerceNonEmptyString(args.title) ?? "Workspace task"; |
| 2925 | const mode = args.workspace?.mode ?? "new"; |
| 2926 | if (mode !== "new" && mode !== "fork" && mode !== "existing") { |
| 2927 | return Err("Task.createWorkspaceTurn: unsupported workspace mode"); |
| 2928 | } |
| 2929 | const queueDispatchMode = args.workspace?.queueDispatchMode ?? "tool-end"; |
| 2930 | if (queueDispatchMode !== "tool-end" && queueDispatchMode !== "turn-end") { |
| 2931 | return Err("Task.createWorkspaceTurn: unsupported queueDispatchMode"); |
| 2932 | } |
| 2933 | |
| 2934 | await using _lock = await this.mutex.acquire(); |
| 2935 | |
| 2936 | const parentMetaResult = await this.aiService.getWorkspaceMetadata(ownerWorkspaceId); |
| 2937 | if (!parentMetaResult.success) { |
| 2938 | return Err(`Task.createWorkspaceTurn: owner workspace not found (${parentMetaResult.error})`); |
| 2939 | } |
| 2940 | const parentMeta = parentMetaResult.data; |
| 2941 | const cfg = this.config.loadConfigOrDefault(); |
| 2942 | const taskSettings = cfg.taskSettings ?? DEFAULT_TASK_SETTINGS; |
| 2943 | const taskProjectConfig = cfg.projects.get(stripTrailingSlashes(parentMeta.projectPath)); |
| 2944 | if ((parentMeta.projects?.length ?? 0) > 1) { |
| 2945 | // WorkspaceService.create only materializes one project checkout; fail loudly instead of |
| 2946 | // silently dropping secondary repos from a multi-project caller's task context. |
| 2947 | return Err("Task.createWorkspaceTurn: multi-project workspace turns are not supported yet"); |
| 2948 | } |
| 2949 | if (!taskProjectConfig?.trusted) { |
| 2950 | return Err( |
| 2951 | "This project must be trusted before creating workspaces. Trust the project in Settings → Security, or create a workspace from the project page." |
| 2952 | ); |
| 2953 | } |
| 2954 | |
| 2955 | const allWorkspaceTurns = await this.taskHandleStore.listAllWorkspaceTurns(); |
| 2956 | const ownerWorkspaceTurns = allWorkspaceTurns.filter( |
| 2957 | (record) => record.ownerWorkspaceId === ownerWorkspaceId |
| 2958 | ); |
| 2959 | const activeAgentCount = this.countActiveAgentTasks(cfg); |
| 2960 | const ensureParallelSlot = async (): Promise<Result<void, string>> => { |
| 2961 | const activeWorkspaceTurnCount = await this.countActiveWorkspaceTurns(allWorkspaceTurns); |
| 2962 | const activeCount = activeAgentCount + activeWorkspaceTurnCount; |
| 2963 | if (activeCount >= taskSettings.maxParallelAgentTasks) { |
| 2964 | return Err( |
| 2965 | `Task.createWorkspaceTurn: maxParallelAgentTasks exceeded (active=${activeCount}, max=${taskSettings.maxParallelAgentTasks})` |
| 2966 | ); |
| 2967 | } |
| 2968 | return Ok(undefined); |
| 2969 | }; |
| 2970 | |