← Back to task

Commit 604c18fd

commit 604c18fd9c5c5699fd0c84eb4ec209ef21e68f9c
Author: Ben Sima <ben@bensima.com>
Date:   Thu Jan 1 13:18:45 2026

    Add /status command to show current orchestrator work (t-280.2.2)
    
    Added orchestrator status tracking:
    - OrchestratorStatus type with task ID, title, phase, iteration, timing
    - Global TVar registry (activeOrchestrators) keyed by chatId
    - register/update/unregister functions
    - getOrchestratorStatus and getActiveOrchestrators queries
    
    Added /status command handler:
    - Responds to /status, status, 'what are you working on'
    - Shows current task, phase, and iteration if working
    - Shows idle message if not
    
    Task-Id: t-280.2.2

diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs
index 3be4ebdd..4a41c497 100644
--- a/Omni/Agent/Telegram.hs
+++ b/Omni/Agent/Telegram.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE NoImplicitPrelude #-}
@@ -62,6 +63,7 @@ module Omni.Agent.Telegram
     lookupChatId,
     handleRemindersCommand,
     handleReadyCommand,
+    handleStatusCommand,
 
     -- * System Prompt
     telegramSystemPrompt,
@@ -99,6 +101,7 @@ import qualified Omni.Agent.Telegram.Actions as Actions
 import qualified Omni.Agent.Telegram.IncomingQueue as IncomingQueue
 import qualified Omni.Agent.Telegram.Media as Media
 import qualified Omni.Agent.Telegram.Messages as Messages
+import qualified Omni.Agent.Telegram.Orchestrator as Orchestrator
 import qualified Omni.Agent.Telegram.Reminders as Reminders
 import qualified Omni.Agent.Telegram.Types as Types
 import qualified Omni.Agent.Tools as Tools
@@ -813,8 +816,9 @@ handleAuthorizedMessage tgConfig provider engineCfg msg uid userName chatId = do
   outreachHandled <- handleOutreachCommand tgConfig chatId threadId msgText
   remindersHandled <- if outreachHandled then pure True else handleRemindersCommand uid chatId threadId msgText
   readyHandled <- if remindersHandled then pure True else handleReadyCommand chatId threadId msgText
+  statusHandled <- if readyHandled then pure True else handleStatusCommand chatId threadId msgText
 
-  unless readyHandled <| handleAuthorizedMessageContinued tgConfig provider engineCfg msg uid userName chatId
+  unless statusHandled <| handleAuthorizedMessageContinued tgConfig provider engineCfg msg uid userName chatId
 
 handleAuthorizedMessageContinued ::
   Types.TelegramConfig ->
@@ -1000,8 +1004,9 @@ handleAuthorizedMessageBatch tgConfig provider engineCfg msg uid userName chatId
   outreachHandled <- handleOutreachCommand tgConfig chatId threadId batchedText
   remindersHandled <- if outreachHandled then pure True else handleRemindersCommand uid chatId threadId batchedText
   readyHandled <- if remindersHandled then pure True else handleReadyCommand chatId threadId batchedText
+  statusHandled <- if readyHandled then pure True else handleStatusCommand chatId threadId batchedText
 
-  unless readyHandled <| handleAuthorizedMessageBatchContinued tgConfig provider engineCfg msg uid userName chatId batchedText
+  unless statusHandled <| handleAuthorizedMessageBatchContinued tgConfig provider engineCfg msg uid userName chatId batchedText
 
 handleAuthorizedMessageBatchContinued ::
   Types.TelegramConfig ->
@@ -1777,6 +1782,40 @@ handleReadyCommand chatId mThreadId cmd
         <> Text.take 50 (Task.taskTitle t)
         <> if Text.length (Task.taskTitle t) > 50 then "..." else ""
 
+-- | Handle /status command - show current orchestrator work
+handleStatusCommand :: Int -> Maybe Int -> Text -> IO Bool
+handleStatusCommand chatId mThreadId cmd
+  | cmd == "/status" || cmd == "/status " || cmd == "status" || cmd == "what are you working on" = do
+      mStatus <- Orchestrator.getOrchestratorStatus chatId
+      let msg = case mStatus of
+            Nothing -> "I'm not working on anything right now. Send me a task ID to start work."
+            Just status -> formatOrchestratorStatus status
+      _ <- Messages.enqueueImmediate Nothing chatId mThreadId msg (Just "system") Nothing
+      pure True
+  | otherwise = pure False
+  where
+    formatOrchestratorStatus :: Orchestrator.OrchestratorStatus -> Text
+    formatOrchestratorStatus status =
+      "🔧 *Working on " <> Orchestrator.osTaskId status <> "*\n"
+        <> Orchestrator.osTaskTitle status <> "\n\n"
+        <> "Phase: " <> formatPhaseSimple (Orchestrator.osPhase status) <> "\n"
+        <> "Iteration: " <> tshow (Orchestrator.osIteration status) <> "/" <> tshow (Orchestrator.osMaxIterations status)
+    
+    formatPhaseSimple :: Orchestrator.OrchestratorPhase -> Text
+    formatPhaseSimple = \case
+      Orchestrator.PhaseStarting -> "Starting"
+      Orchestrator.PhaseCoder _ -> "Coder running"
+      Orchestrator.PhaseCoderDone _ -> "Coder done"
+      Orchestrator.PhaseVerifyBuild _ -> "Verifying build"
+      Orchestrator.PhaseBuildPassed _ -> "Build passed"
+      Orchestrator.PhaseBuildFailed _ _ -> "Build failed"
+      Orchestrator.PhaseReviewer _ -> "Reviewer running"
+      Orchestrator.PhaseReviewerApproved -> "Reviewer approved"
+      Orchestrator.PhaseReviewerRejected _ _ -> "Reviewer rejected"
+      Orchestrator.PhaseCommitting -> "Committing"
+      Orchestrator.PhaseComplete _ -> "Complete"
+      Orchestrator.PhaseFailed _ -> "Failed"
+
 -- | Handle /reminders commands
 --   /reminders              - List active reminders
 --   /reminders add <time> <message> - Add new reminder
diff --git a/Omni/Agent/Telegram/Orchestrator.hs b/Omni/Agent/Telegram/Orchestrator.hs
index fd31e222..d063d17e 100644
--- a/Omni/Agent/Telegram/Orchestrator.hs
+++ b/Omni/Agent/Telegram/Orchestrator.hs
@@ -31,6 +31,11 @@ module Omni.Agent.Telegram.Orchestrator
     spawnOrchestrator,
     runOrchestrator,
 
+    -- * Status Tracking
+    OrchestratorStatus (..),
+    getActiveOrchestrators,
+    getOrchestratorStatus,
+
     -- * Testing
     main,
     test,
@@ -51,6 +56,10 @@ import qualified System.Exit as Exit
 import qualified System.Process as Process
 import qualified System.Timeout as Timeout
 import System.IO (hFlush)
+import qualified Control.Concurrent.STM as STM
+import qualified Data.Map.Strict as Map
+import qualified Data.Time as Time
+import System.IO.Unsafe (unsafePerformIO)
 
 -- | Configuration for an orchestrator run
 data OrchestratorConfig = OrchestratorConfig
@@ -77,6 +86,46 @@ defaultConfig taskId chatId =
       orchWorkDir = "/home/ben/omni/ava"
     }
 
+-- | Status of an active orchestrator job
+data OrchestratorStatus = OrchestratorStatus
+  { osTaskId :: Text,
+    osTaskTitle :: Text,
+    osChatId :: Int,
+    osPhase :: OrchestratorPhase,
+    osIteration :: Int,
+    osMaxIterations :: Int,
+    osStartedAt :: Time.UTCTime
+  }
+  deriving (Show, Generic)
+
+-- | Global registry of active orchestrators (keyed by chatId)
+{-# NOINLINE activeOrchestrators #-}
+activeOrchestrators :: STM.TVar (Map.Map Int OrchestratorStatus)
+activeOrchestrators = unsafePerformIO <| STM.newTVarIO Map.empty
+
+-- | Register an orchestrator as active
+registerOrchestrator :: OrchestratorStatus -> IO ()
+registerOrchestrator status = STM.atomically <|
+  STM.modifyTVar' activeOrchestrators (Map.insert (osChatId status) status)
+
+-- | Update the phase of an active orchestrator
+updateOrchestratorPhase :: Int -> OrchestratorPhase -> Int -> IO ()
+updateOrchestratorPhase chatId newPhase iteration = STM.atomically <|
+  STM.modifyTVar' activeOrchestrators (Map.adjust (\s -> s { osPhase = newPhase, osIteration = iteration }) chatId)
+
+-- | Remove an orchestrator from active registry
+unregisterOrchestrator :: Int -> IO ()
+unregisterOrchestrator chatId = STM.atomically <|
+  STM.modifyTVar' activeOrchestrators (Map.delete chatId)
+
+-- | Get all active orchestrators
+getActiveOrchestrators :: IO [OrchestratorStatus]
+getActiveOrchestrators = Map.elems <$> STM.readTVarIO activeOrchestrators
+
+-- | Get orchestrator status for a specific chat
+getOrchestratorStatus :: Int -> IO (Maybe OrchestratorStatus)
+getOrchestratorStatus chatId = Map.lookup chatId <$> STM.readTVarIO activeOrchestrators
+
 -- | Current phase of the orchestrator
 data OrchestratorPhase
   = PhaseStarting
@@ -126,9 +175,29 @@ spawnOrchestrator :: Types.TelegramConfig -> OrchestratorConfig -> IO (Async.Asy
 spawnOrchestrator tgCfg cfg = do
   putText <| "spawnOrchestrator: About to spawn async for " <> orchTaskId cfg
   hFlush stdout
+  
+  -- Look up task title for status display
+  allTasks <- Task.loadTasks
+  let mTask = Task.findTask (orchTaskId cfg) allTasks
+      taskTitle = maybe "Unknown task" Task.taskTitle mTask
+  
   Async.async <| do
     putText <| "Orchestrator async thread started for " <> orchTaskId cfg
     hFlush stdout
+    
+    -- Register this orchestrator
+    now <- Time.getCurrentTime
+    let status = OrchestratorStatus
+          { osTaskId = orchTaskId cfg,
+            osTaskTitle = taskTitle,
+            osChatId = orchChatId cfg,
+            osPhase = PhaseStarting,
+            osIteration = 0,
+            osMaxIterations = orchMaxIterations cfg,
+            osStartedAt = now
+          }
+    registerOrchestrator status
+    
     -- Create initial status message
     mMsgId <- createStatusMessage tgCfg (orchChatId cfg) (orchThreadId cfg)
       (formatPhase (orchTaskId cfg) (orchMaxIterations cfg) PhaseStarting)
@@ -146,7 +215,8 @@ spawnOrchestrator tgCfg cfg = do
         result <- try @SomeException (runOrchestrator tgCfg cfg msgId)
         case result of
           Left err -> do
-            -- Orchestrator crashed
+            -- Orchestrator crashed - unregister and report error
+            unregisterOrchestrator (orchChatId cfg)
             updateStatusMessage tgCfg (orchChatId cfg) msgId
               (formatPhase (orchTaskId cfg) (orchMaxIterations cfg) (PhaseFailed (tshow err)))
             _ <- sendMessage tgCfg (orchChatId cfg) (orchThreadId cfg)
@@ -158,22 +228,27 @@ spawnOrchestrator tgCfg cfg = do
 runOrchestrator :: Types.TelegramConfig -> OrchestratorConfig -> Int -> IO ()
 runOrchestrator tgCfg cfg statusMsgId = do
   let tid = orchTaskId cfg
+      chatId = orchChatId cfg
       maxIter = orchMaxIterations cfg
-      updatePhase p = updateStatusMessage tgCfg (orchChatId cfg) statusMsgId
-        (formatPhase tid maxIter p)
-      sendResult msg = sendMessage tgCfg (orchChatId cfg) (orchThreadId cfg) msg
+      updatePhase p iter = do
+        updateStatusMessage tgCfg chatId statusMsgId (formatPhase tid maxIter p)
+        updateOrchestratorPhase chatId p iter
+      sendResult msg = sendMessage tgCfg chatId (orchThreadId cfg) msg
   
   -- Run iterations
   result <- runLoop 1
   
+  -- Unregister this orchestrator
+  unregisterOrchestrator chatId
+  
   -- Send final result as new message
   case result of
     Left err -> do
-      updatePhase (PhaseFailed err)
+      updatePhase (PhaseFailed err) 0
       _ <- sendResult ("❌ " <> tid <> " failed after " <> tshow maxIter <> " iterations: " <> err)
       pure ()
     Right commit -> do
-      updatePhase (PhaseComplete commit)
+      updatePhase (PhaseComplete commit) 0
       _ <- sendResult ("✅ " <> tid <> " complete! Commit: " <> commit)
       -- Mark task as done
       Task.updateTaskStatus tid Task.Done []
@@ -184,8 +259,11 @@ runOrchestrator tgCfg cfg statusMsgId = do
       | iteration > orchMaxIterations cfg = 
           pure (Left "Max iterations exceeded")
       | otherwise = do
-          let updatePhase' p = updateStatusMessage tgCfg (orchChatId cfg) statusMsgId
-                (formatPhase (orchTaskId cfg) (orchMaxIterations cfg) p)
+          let chatId = orchChatId cfg
+              updatePhase' p = do
+                updateStatusMessage tgCfg chatId statusMsgId
+                  (formatPhase (orchTaskId cfg) (orchMaxIterations cfg) p)
+                updateOrchestratorPhase chatId p iteration
           
           -- Phase: Coder
           updatePhase' (PhaseCoder iteration)