(
ancestorWorkspaceId: string,
taskId: string
)
| 3719 | } |
| 3720 | |
| 3721 | async terminateDescendantAgentTask( |
| 3722 | ancestorWorkspaceId: string, |
| 3723 | taskId: string |
| 3724 | ): Promise<Result<TerminateAgentTaskResult, string>> { |
| 3725 | assert( |
| 3726 | ancestorWorkspaceId.length > 0, |
| 3727 | "terminateDescendantAgentTask: ancestorWorkspaceId must be non-empty" |
| 3728 | ); |
| 3729 | assert(taskId.length > 0, "terminateDescendantAgentTask: taskId must be non-empty"); |
| 3730 | |
| 3731 | const terminatedTaskIds: string[] = []; |
| 3732 | |
| 3733 | { |
| 3734 | await using _lock = await this.mutex.acquire(); |
| 3735 | |
| 3736 | const cfg = this.config.loadConfigOrDefault(); |
| 3737 | const entry = findWorkspaceEntry(cfg, taskId); |
| 3738 | if (!entry?.workspace.parentWorkspaceId) { |
| 3739 | return Err("Task not found"); |
| 3740 | } |
| 3741 | |
| 3742 | const index = this.buildAgentTaskIndex(cfg); |
| 3743 | if ( |
| 3744 | !this.isDescendantAgentTaskUsingParentById(index.parentById, ancestorWorkspaceId, taskId) |
| 3745 | ) { |
| 3746 | return Err("Task is not a descendant of this workspace"); |
| 3747 | } |
| 3748 | |
| 3749 | // Terminate the entire subtree to avoid orphaned descendant tasks. |
| 3750 | const descendants = this.listDescendantAgentTaskIdsFromIndex(index, taskId); |
| 3751 | const toTerminate = Array.from(new Set([taskId, ...descendants])); |
| 3752 | |
| 3753 | // Delete leaves first to avoid leaving children with missing parents. |
| 3754 | const parentById = index.parentById; |
| 3755 | const depthById = new Map<string, number>(); |
| 3756 | for (const id of toTerminate) { |
| 3757 | depthById.set(id, this.getTaskDepthFromParentById(parentById, id)); |
| 3758 | } |
| 3759 | toTerminate.sort((a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)); |
| 3760 | |
| 3761 | const terminationError = new Error("Task terminated"); |
| 3762 | |
| 3763 | for (const id of toTerminate) { |
| 3764 | // Best-effort: stop any active stream immediately to avoid further token usage. |
| 3765 | try { |
| 3766 | const stopResult = await this.aiService.stopStream(id, { abandonPartial: true }); |
| 3767 | if (!stopResult.success) { |
| 3768 | log.debug("terminateDescendantAgentTask: stopStream failed", { taskId: id }); |
| 3769 | } |
| 3770 | } catch (error: unknown) { |
| 3771 | log.debug("terminateDescendantAgentTask: stopStream threw", { taskId: id, error }); |
| 3772 | } |
| 3773 | |
| 3774 | this.completedReportsByTaskId.delete(id); |
| 3775 | this.rejectWaiters(id, terminationError); |
| 3776 | |
| 3777 | const removeResult = await this.workspaceService.remove(id, true); |
| 3778 | if (!removeResult.success) { |
no test coverage detected