fix(shell): timeout is recoverable + coaches detach, not a workflow kill
A foreground long-running command (npm run dev, vite, a --watch task) hit the 30s timeout and returned recoverable=false, giving the stage no transition to take -> WorkflowFailed "no transition condition matched" (killed session d734e1de). A non-zero exit right below already returns recoverable=true; a timeout is no more fatal. Return recoverable and coach the model to start the process detached (nohup ... &) and verify separately, or run a one-shot that exits. No background-process registry: the model backgrounds it itself via `&`, which already routes through `sh -c`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HMbPmZZjcXhR2crU82zZ8S
This commit is contained in:
+12
-2
@@ -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 <cmd> > /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()
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user