* Interrupt all descendant agent tasks for a workspace (leaf-first). * * Rationale: when a user hard-interrupts a parent workspace, descendants must * also stop so they cannot later auto-resume the interrupted parent. * * Keep interrupted task workspaces on disk so users can inspect o
(
workspaceId: string,
options?: { workflowRunId?: string }
)
| 3824 | * compatibility with existing call sites. |
| 3825 | */ |
| 3826 | async terminateAllDescendantAgentTasks( |
| 3827 | workspaceId: string, |
| 3828 | options?: { workflowRunId?: string } |
| 3829 | ): Promise<string[]> { |
| 3830 | assert( |
| 3831 | workspaceId.length > 0, |
| 3832 | "terminateAllDescendantAgentTasks: workspaceId must be non-empty" |
| 3833 | ); |
| 3834 | |
| 3835 | const interruptedTaskIds: string[] = []; |
| 3836 | |
| 3837 | { |
| 3838 | await using _lock = await this.mutex.acquire(); |
| 3839 | |
| 3840 | const cfg = this.config.loadConfigOrDefault(); |
| 3841 | const index = this.buildAgentTaskIndex(cfg); |
| 3842 | const descendants = this.listDescendantAgentTaskIdsFromIndex(index, workspaceId).filter( |
| 3843 | (taskId) => |
| 3844 | options?.workflowRunId == null || |
| 3845 | this.isWorkflowRunDescendant(index, taskId, options.workflowRunId) |
| 3846 | ); |
| 3847 | if (descendants.length === 0) { |
| 3848 | return interruptedTaskIds; |
| 3849 | } |
| 3850 | |
| 3851 | // Interrupt leaves first to avoid descendant/ancestor status races. |
| 3852 | const parentById = index.parentById; |
| 3853 | const depthById = new Map<string, number>(); |
| 3854 | for (const id of descendants) { |
| 3855 | depthById.set(id, this.getTaskDepthFromParentById(parentById, id)); |
| 3856 | } |
| 3857 | descendants.sort((a, b) => (depthById.get(b) ?? 0) - (depthById.get(a) ?? 0)); |
| 3858 | |
| 3859 | const interruptionError = new Error("Parent workspace interrupted"); |
| 3860 | |
| 3861 | for (const id of descendants) { |
| 3862 | // Best-effort: clear queue first. AgentSession stream-end cleanup auto-flushes |
| 3863 | // queued messages, so descendants must not keep pending input after a hard interrupt. |
| 3864 | try { |
| 3865 | const clearQueueResult = this.workspaceService.clearQueue(id); |
| 3866 | if (!clearQueueResult.success) { |
| 3867 | log.debug("terminateAllDescendantAgentTasks: clearQueue failed", { |
| 3868 | taskId: id, |
| 3869 | error: clearQueueResult.error, |
| 3870 | }); |
| 3871 | } |
| 3872 | } catch (error: unknown) { |
| 3873 | log.debug("terminateAllDescendantAgentTasks: clearQueue threw", { taskId: id, error }); |
| 3874 | } |
| 3875 | |
| 3876 | // Best-effort: stop any active stream immediately to avoid further token usage |
| 3877 | // while preserving commit-worthy partial progress for inspection/resume. |
| 3878 | try { |
| 3879 | const stopResult = await this.aiService.stopStream(id, { abandonPartial: false }); |
| 3880 | if (!stopResult.success) { |
| 3881 | log.debug("terminateAllDescendantAgentTasks: stopStream failed", { taskId: id }); |
| 3882 | } |
| 3883 | } catch (error: unknown) { |
no test coverage detected