(taskId: string, description: string, kind: BashTaskKind | undefined, toolUseId?: string, agentId?: AgentId)
| 44 | // Output-side analog of peekForStdinData (utils/process.ts): fire a one-shot |
| 45 | // notification if output stops growing and the tail looks like a prompt. |
| 46 | function startStallWatchdog(taskId: string, description: string, kind: BashTaskKind | undefined, toolUseId?: string, agentId?: AgentId): () => void { |
| 47 | if (kind === 'monitor') return () => {}; |
| 48 | const outputPath = getTaskOutputPath(taskId); |
| 49 | let lastSize = 0; |
| 50 | let lastGrowth = Date.now(); |
| 51 | let cancelled = false; |
| 52 | const timer = setInterval(() => { |
| 53 | void stat(outputPath).then(s => { |
| 54 | if (s.size > lastSize) { |
| 55 | lastSize = s.size; |
| 56 | lastGrowth = Date.now(); |
| 57 | return; |
| 58 | } |
| 59 | if (Date.now() - lastGrowth < STALL_THRESHOLD_MS) return; |
| 60 | void tailFile(outputPath, STALL_TAIL_BYTES).then(({ |
| 61 | content |
| 62 | }) => { |
| 63 | if (cancelled) return; |
| 64 | if (!looksLikePrompt(content)) { |
| 65 | // Not a prompt — keep watching. Reset so the next check is |
| 66 | // 45s out instead of re-reading the tail on every tick. |
| 67 | lastGrowth = Date.now(); |
| 68 | return; |
| 69 | } |
| 70 | // Latch before the async-boundary-visible side effects so an |
| 71 | // overlapping tick's callback sees cancelled=true and bails. |
| 72 | cancelled = true; |
| 73 | clearInterval(timer); |
| 74 | const toolUseIdLine = toolUseId ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>` : ''; |
| 75 | const summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" appears to be waiting for interactive input`; |
| 76 | // No <status> tag — print.ts treats <status> as a terminal |
| 77 | // signal and an unknown value falls through to 'completed', |
| 78 | // falsely closing the task for SDK consumers. Statusless |
| 79 | // notifications are skipped by the SDK emitter (progress ping). |
| 80 | const message = `<${TASK_NOTIFICATION_TAG}> |
| 81 | <${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine} |
| 82 | <${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}> |
| 83 | <${SUMMARY_TAG}>${escapeXml(summary)}</${SUMMARY_TAG}> |
| 84 | </${TASK_NOTIFICATION_TAG}> |
| 85 | Last output: |
| 86 | ${content.trimEnd()} |
| 87 | |
| 88 | The command is likely blocked on an interactive prompt. Kill this task and re-run with piped input (e.g., \`echo y | command\`) or a non-interactive flag if one exists.`; |
| 89 | enqueuePendingNotification({ |
| 90 | value: message, |
| 91 | mode: 'task-notification', |
| 92 | priority: 'next', |
| 93 | agentId |
| 94 | }); |
| 95 | }, () => {}); |
| 96 | }, () => {} // File may not exist yet |
| 97 | ); |
| 98 | }, STALL_CHECK_INTERVAL_MS); |
| 99 | timer.unref(); |
| 100 | return () => { |
| 101 | cancelled = true; |
| 102 | clearInterval(timer); |
| 103 | }; |
no test coverage detected