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