(
workspaceId: string,
options?: {
reason?: "startup" | "stream_end" | "error";
error?: Pick<ErrorEvent, "error" | "errorType">;
}
)
| 7404 | } |
| 7405 | |
| 7406 | private async promptTaskForRequiredCompletionTool( |
| 7407 | workspaceId: string, |
| 7408 | options?: { |
| 7409 | reason?: "startup" | "stream_end" | "error"; |
| 7410 | error?: Pick<ErrorEvent, "error" | "errorType">; |
| 7411 | } |
| 7412 | ): Promise<boolean> { |
| 7413 | assert( |
| 7414 | workspaceId.length > 0, |
| 7415 | "promptTaskForRequiredCompletionTool: workspaceId must be non-empty" |
| 7416 | ); |
| 7417 | |
| 7418 | const cfg = this.config.loadConfigOrDefault(); |
| 7419 | const entry = findWorkspaceEntry(cfg, workspaceId); |
| 7420 | if (!entry?.workspace.parentWorkspaceId) { |
| 7421 | return false; |
| 7422 | } |
| 7423 | if (entry.workspace.taskStatus !== "awaiting_report") { |
| 7424 | return false; |
| 7425 | } |
| 7426 | const taskIndex = this.buildAgentTaskIndex(cfg); |
| 7427 | if ( |
| 7428 | await this.interruptTaskRecoveryForInactiveWorkflowOwner( |
| 7429 | workspaceId, |
| 7430 | cfg, |
| 7431 | `completion-tool-${options?.reason ?? "unknown"}`, |
| 7432 | taskIndex |
| 7433 | ) |
| 7434 | ) { |
| 7435 | return false; |
| 7436 | } |
| 7437 | if (await this.hasActiveTaskOwnedWork(workspaceId, taskIndex)) { |
| 7438 | return false; |
| 7439 | } |
| 7440 | if (this.aiService.isStreaming(workspaceId)) { |
| 7441 | return true; |
| 7442 | } |
| 7443 | |
| 7444 | const isPlanLike = await this.isPlanLikeTaskWorkspace(entry); |
| 7445 | const completionToolName = isPlanLike ? "propose_plan" : "agent_report"; |
| 7446 | |
| 7447 | // Persisted circuit breaker: a task that keeps consuming recovery prompts |
| 7448 | // without ever completing is stuck (repeated empty output, repeated |
| 7449 | // length-truncated turns, or a model that never calls its completion |
| 7450 | // tool). Interrupt it with a descriptive error instead of prompting |
| 7451 | // forever. The counter lives on the workspace entry so restart loops stay |
| 7452 | // bounded too; finalizeAgentTaskReport clears it on success. |
| 7453 | const recoveryAttempts = entry.workspace.taskRecoveryAttempts ?? 0; |
| 7454 | if (recoveryAttempts >= MAX_TASK_RECOVERY_ATTEMPTS) { |
| 7455 | const lastError = options?.error |
| 7456 | ? ` Last error (${options.error.errorType ?? "unknown"}): ${options.error.error}` |
| 7457 | : ""; |
| 7458 | log.error("Task exceeded its recovery attempt budget; interrupting task", { |
| 7459 | workspaceId, |
| 7460 | taskName: entry.workspace.name, |
| 7461 | recoveryAttempts, |
| 7462 | limit: MAX_TASK_RECOVERY_ATTEMPTS, |
| 7463 | reason: options?.reason, |
no test coverage detected