(
workspaceId: string,
options?: { soft?: boolean; abandonPartial?: boolean; sendQueuedImmediately?: boolean }
)
| 7524 | } |
| 7525 | |
| 7526 | async interruptStream( |
| 7527 | workspaceId: string, |
| 7528 | options?: { soft?: boolean; abandonPartial?: boolean; sendQueuedImmediately?: boolean } |
| 7529 | ): Promise<Result<void>> { |
| 7530 | try { |
| 7531 | this.taskService?.resetAutoResumeCount(workspaceId); |
| 7532 | if (!options?.soft) { |
| 7533 | // Mark before attempting the session interrupt to close races where a child |
| 7534 | // could report between stop initiation and descendant cascade termination. |
| 7535 | this.taskService?.markParentWorkspaceInterrupted(workspaceId); |
| 7536 | } |
| 7537 | |
| 7538 | const session = this.getOrCreateSession(workspaceId); |
| 7539 | const stopResult = await session.interruptStream(options); |
| 7540 | if (!stopResult.success) { |
| 7541 | // Interrupt failed, so clear hard-interrupt suppression we set above. |
| 7542 | if (!options?.soft) { |
| 7543 | this.taskService?.resetAutoResumeCount(workspaceId); |
| 7544 | } |
| 7545 | log.error("Failed to stop stream:", stopResult.error); |
| 7546 | return Err(stopResult.error); |
| 7547 | } |
| 7548 | |
| 7549 | // For hard interrupts, delete partial immediately. For soft interrupts, |
| 7550 | // defer to stream-abort handler (stream is still running and may recreate partial). |
| 7551 | if (options?.abandonPartial && !options?.soft) { |
| 7552 | log.debug("Abandoning partial for workspace:", workspaceId); |
| 7553 | await this.historyService.deletePartial(workspaceId); |
| 7554 | } |
| 7555 | |
| 7556 | // Rationale: user-initiated hard interrupts should stop the entire task tree so |
| 7557 | // descendant sub-agents cannot finish later and auto-resume this workspace. |
| 7558 | if (!options?.soft) { |
| 7559 | try { |
| 7560 | const interruptedTaskIds = |
| 7561 | await this.taskService?.terminateAllDescendantAgentTasks?.(workspaceId); |
| 7562 | if (interruptedTaskIds && interruptedTaskIds.length > 0) { |
| 7563 | log.debug("Cascade-interrupted descendant tasks on interrupt", { |
| 7564 | workspaceId, |
| 7565 | interruptedTaskIds, |
| 7566 | }); |
| 7567 | } |
| 7568 | } catch (error: unknown) { |
| 7569 | log.error("Failed to cascade-interrupt descendant tasks on interrupt", { |
| 7570 | workspaceId, |
| 7571 | error, |
| 7572 | }); |
| 7573 | } |
| 7574 | } |
| 7575 | |
| 7576 | // Handle queued messages based on option |
| 7577 | if (options?.sendQueuedImmediately) { |
| 7578 | // `sendQueuedMessages()` routes through AgentSession directly, so explicitly |
| 7579 | // clear hard-interrupt suppression first (it won't flow through sendMessage()). |
| 7580 | this.taskService?.resetAutoResumeCount(workspaceId); |
| 7581 | // Send queued messages immediately instead of restoring to input |
| 7582 | session.sendQueuedMessages(); |
| 7583 | } else { |
no test coverage detected