diff --git a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt index 4b0b8b0e..d6ac43ec 100644 --- a/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt +++ b/infrastructure/tools/src/main/kotlin/com/correx/infrastructure/tools/shell/ShellTool.kt @@ -293,10 +293,20 @@ class ShellTool( killTree(process) stdoutDeferred.cancel() stderrDeferred.cancel() + // A timeout is NOT fatal — it's usually a long-running/watch/server command (npm run dev, + // vite, a watcher) run in the foreground, where it never exits. Return recoverable so the + // stage keeps going and coach the model to detach it, instead of killing the workflow with + // a dead-end failure (no transition matched). ponytail: no background-process registry — + // the model backgrounds it itself via `&`/nohup, which already routes through `sh -c`. return@coroutineScope ToolResult.Failure( invocationId = request.invocationId, - reason = "Process timed out after ${timeoutMs}ms", - recoverable = false, + reason = "Process did not exit within ${timeoutMs}ms and was killed. If this is a " + + "long-running command (a dev server, `npm run dev`/`vite`/`serve`, a `--watch` " + + "task), start it detached so it returns immediately — e.g. " + + "`nohup > /tmp/dev.log 2>&1 &` — then verify separately (curl the port, or " + + "read the log). If you only needed its output, run a one-shot command that exits " + + "(e.g. `npm run build`, not `npm run dev`).", + recoverable = true, ) } val exitCode = process.exitValue() diff --git a/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt b/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt index efaec0ee..77b2b975 100644 --- a/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt +++ b/infrastructure/tools/src/test/kotlin/com/correx/infrastructure/tools/shell/ShellToolTest.kt @@ -155,8 +155,9 @@ class ShellToolTest { assertTrue(result is ToolResult.Failure) val failure = result as ToolResult.Failure - assertEquals("Process timed out after 100ms", failure.reason) - assertFalse(failure.recoverable) + assertTrue(failure.reason.contains("did not exit within 100ms"), failure.reason) + assertTrue(failure.reason.contains("detached"), "must coach the model to background it") + assertTrue(failure.recoverable, "a timeout is a long-running command, not a fatal error") } @Test