* Execute a tool with a hard settlement guarantee. If the handler neither * resolves nor rejects within the tool's watchdog cap, throw a timeout error * so the standard failure path (persist failed row, publish terminal * confirmation, resume Go with an error result) runs and the chat never * we
(toolCall: ToolCallState, execContext: ExecutionContext)
| 264 | * eventual settlement is ignored. |
| 265 | */ |
| 266 | async function executeToolWithWatchdog(toolCall: ToolCallState, execContext: ExecutionContext) { |
| 267 | const timeoutMs = toolWatchdogTimeoutMs(toolCall.name) |
| 268 | // Thread the invoking subagent's channel id per call (execContext is shared |
| 269 | // across the whole turn, so the channel id can't live on it) — server tools |
| 270 | // use it to scope the workspace_file -> edit_content intent handoff. |
| 271 | const toolContext = toolCall.parentToolCallId |
| 272 | ? { ...execContext, parentToolCallId: toolCall.parentToolCallId } |
| 273 | : execContext |
| 274 | const execution = executeTool(toolCall.name, toolCall.params || {}, toolContext) |
| 275 | let timer: ReturnType<typeof setTimeout> | undefined |
| 276 | try { |
| 277 | return await Promise.race([ |
| 278 | execution, |
| 279 | new Promise<never>((_, reject) => { |
| 280 | timer = setTimeout( |
| 281 | () => reject(new ToolExecutionTimeoutError(toolCall.name, timeoutMs)), |
| 282 | timeoutMs |
| 283 | ) |
| 284 | }), |
| 285 | ]) |
| 286 | } finally { |
| 287 | if (timer) clearTimeout(timer) |
| 288 | // Swallow the abandoned promise's eventual rejection so it can't surface |
| 289 | // as an unhandled rejection after a watchdog loss. |
| 290 | execution.catch(() => {}) |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Last-resort settlement for a tool whose promise never settled (a hang the |
no test coverage detected