← Back to task

Commit 3c670ea3

commit 3c670ea3a4ddbb24152cdbcc0b051550990f013a
Author: Ben Sima <ben@bensima.com>
Date:   Tue Dec 30 23:58:07 2025

    Omni/Agent/Tools/Tasks.hs: Handle orchestrator spawn failures gracefully
    
    Automated via pi-review.
    
    Task-Id: t-298.3

diff --git a/Omni/Agent/Tools/Tasks.hs b/Omni/Agent/Tools/Tasks.hs
index fa21af87..0daf1374 100644
--- a/Omni/Agent/Tools/Tasks.hs
+++ b/Omni/Agent/Tools/Tasks.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE NoImplicitPrelude #-}
@@ -23,13 +22,14 @@ module Omni.Agent.Tools.Tasks
 where
 
 import Alpha
-import Data.Aeson ((.=), (.:))
+import qualified Control.Concurrent as Concurrent
+import Data.Aeson ((.:), (.=))
 import qualified Data.Aeson as Aeson
 import qualified Data.Text as Text
-
 import qualified Omni.Agent.Engine as Engine
 import qualified Omni.Task.Core as Task
 import qualified Omni.Test as Test
+import qualified System.Exit as Exit
 import qualified System.Process as Process
 
 -- | Arguments for work_on_task tool
@@ -39,8 +39,9 @@ newtype WorkOnTaskArgs = WorkOnTaskArgs
   deriving (Show)
 
 instance Aeson.FromJSON WorkOnTaskArgs where
-  parseJSON = Aeson.withObject "WorkOnTaskArgs" <| \v ->
-    WorkOnTaskArgs <$> v .: "task_id"
+  parseJSON =
+    Aeson.withObject "WorkOnTaskArgs" <| \v ->
+      WorkOnTaskArgs </ (v .: "task_id")
 
 main :: IO ()
 main = Test.run test
@@ -100,18 +101,55 @@ executeWorkOnTask v = do
               if status `notElem` [Task.Open, Task.InProgress, Task.Draft]
                 then pure <| Aeson.object ["error" .= ("Task " <> tid <> " is not in a workable state (status: " <> tshow status <> ")")]
                 else do
-                  -- Spawn orchestrator in background
-                  _ <-
-                    Process.spawnProcess
-                      "/home/ava/omni/Omni/Ide/pi-orchestrate.sh"
-                      [Text.unpack tid]
-                  pure <|
-                    Aeson.object
-                      [ "success" .= True,
-                        "task_id" .= tid,
-                        "title" .= Task.taskTitle task,
-                        "message" .= ("Started coder/reviewer loop for " <> tid <> ": " <> Task.taskTitle task)
-                      ]
+                  -- Spawn orchestrator and check for early failures
+                  spawnResult <- spawnOrchestrator tid
+                  case spawnResult of
+                    Left err ->
+                      pure <| Aeson.object ["error" .= err]
+                    Right () ->
+                      pure
+                        <| Aeson.object
+                          [ "success" .= True,
+                            "task_id" .= tid,
+                            "title" .= Task.taskTitle task,
+                            "message" .= ("Started coder/reviewer loop for " <> tid <> ": " <> Task.taskTitle task)
+                          ]
+
+-- | Spawn the orchestrator process and check for early failures.
+-- Returns Left with error message if spawn fails or process exits immediately.
+-- Returns Right () if process is still running after startup delay.
+spawnOrchestrator :: Text -> IO (Either Text ())
+spawnOrchestrator tid = do
+  let scriptPath = "/home/ava/omni/Omni/Ide/pi-orchestrate.sh"
+      createProc =
+        (Process.proc scriptPath [Text.unpack tid])
+          { Process.std_out = Process.NoStream,
+            Process.std_err = Process.NoStream,
+            -- Detach from controlling terminal so it keeps running
+            Process.new_session = True,
+            Process.cwd = Just "/home/ava/omni"
+          }
+
+  -- Try to create the process
+  result <- try @SomeException (Process.createProcess createProc)
+  case result of
+    Left err ->
+      pure <| Left ("Failed to spawn orchestrator: " <> tshow err)
+    Right (_, _, _, ph) -> do
+      -- Wait 500ms to catch immediate failures (missing script, permission errors, etc)
+      Concurrent.threadDelay 500000
+
+      -- Check if process exited early
+      maybeExit <- Process.getProcessExitCode ph
+      case maybeExit of
+        Nothing ->
+          -- Process still running - success!
+          pure (Right ())
+        Just Exit.ExitSuccess ->
+          -- Exited immediately with success - suspicious but ok
+          pure (Right ())
+        Just (Exit.ExitFailure code) ->
+          pure <| Left ("Orchestrator failed to start (exit code " <> tshow code <> ")")
 
 -- | Tool to list tasks ready for work
 listReadyTasksTool :: Engine.Tool
@@ -134,8 +172,8 @@ executeListReadyTasks :: Aeson.Value -> IO Aeson.Value
 executeListReadyTasks _ = do
   readyTasks <- Task.getReadyTasks
   let formatted = map formatTask readyTasks
-  pure <|
-    Aeson.object
+  pure
+    <| Aeson.object
       [ "success" .= True,
         "count" .= length readyTasks,
         "tasks" .= formatted