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)