commit f4a2c7f5cebaf99c8ee37b789b45432fcd3daeee
Author: Coder Agent <coder@agents.omni>
Date: Thu Apr 9 11:15:37 2026
agentd: add updated_at to agent status model and DB updates
Add aiUpdatedAt to AgentInfo and include updated_at in status JSON output.
Extend the agents schema with updated_at and add a safe migration for
existing databases.
Populate updated_at from SELECT queries and FromRow decoding.
Track updated_at on every agent row mutation by touching timestamp state
on status/summary/title/pid updates and setting it inline on started/
completed transitions.
Also set updated_at on agent inserts and on persistent stop updates.
Add a JSON serialization assertion that AgentInfo emits updated_at.
Task-Id: t-764
diff --git a/Omni/Agentd/Daemon.hs b/Omni/Agentd/Daemon.hs
index 0563d0d8..c3bdfa2a 100644
--- a/Omni/Agentd/Daemon.hs
+++ b/Omni/Agentd/Daemon.hs
@@ -306,6 +306,7 @@ data AgentInfo = AgentInfo
aiWorkspace :: Text,
aiPid :: Maybe Int,
aiStartedAt :: Maybe Time.UTCTime,
+ aiUpdatedAt :: Maybe Time.UTCTime,
aiCompletedAt :: Maybe Time.UTCTime,
aiError :: Maybe Text,
aiSummary :: Maybe Text,
@@ -394,6 +395,7 @@ instance ToJSON AgentInfo where
"workspace" .= aiWorkspace info,
"pid" .= aiPid info,
"started_at" .= aiStartedAt info,
+ "updated_at" .= aiUpdatedAt info,
"completed_at" .= aiCompletedAt info,
"error" .= aiError info,
"summary" .= aiSummary info,
@@ -486,6 +488,7 @@ initDb conn = do
\ pid INTEGER,\
\ status TEXT NOT NULL DEFAULT 'pending',\
\ started_at TEXT,\
+ \ updated_at TEXT,\
\ completed_at TEXT,\
\ error TEXT,\
\ summary TEXT,\
@@ -512,6 +515,7 @@ initDb conn = do
_ <- try @SomeException (SQL.execute_ conn sql)
pure ()
addColumn "ALTER TABLE agents ADD COLUMN pid INTEGER"
+ addColumn "ALTER TABLE agents ADD COLUMN updated_at TEXT"
addColumn "ALTER TABLE agents ADD COLUMN summary TEXT"
addColumn "ALTER TABLE agents ADD COLUMN cost_cents INTEGER"
addColumn "ALTER TABLE agents ADD COLUMN webhook_url TEXT"
@@ -601,10 +605,19 @@ initDb conn = do
insertAgent :: SQL.Connection -> Text -> Text -> Text -> Maybe Text -> IO ()
insertAgent conn runId prompt workspace webhookUrl = do
+ now <- Time.getCurrentTime
+ SQL.execute
+ conn
+ "INSERT INTO agents (run_id, prompt, workspace, status, webhook_url, updated_at) VALUES (?, ?, ?, 'pending', ?, ?)"
+ (runId, prompt, workspace, webhookUrl, tshow now)
+
+touchUpdated :: SQL.Connection -> Text -> IO ()
+touchUpdated conn runId = do
+ now <- Time.getCurrentTime
SQL.execute
conn
- "INSERT INTO agents (run_id, prompt, workspace, status, webhook_url) VALUES (?, ?, ?, 'pending', ?)"
- (runId, prompt, workspace, webhookUrl)
+ "UPDATE agents SET updated_at = ? WHERE run_id = ?"
+ (tshow now, runId)
updateAgentPid :: SQL.Connection -> Text -> Int -> IO ()
updateAgentPid conn runId pid = do
@@ -612,6 +625,7 @@ updateAgentPid conn runId pid = do
conn
"UPDATE agents SET pid = ? WHERE run_id = ?"
(pid, runId)
+ touchUpdated conn runId
updateAgentStatus :: SQL.Connection -> Text -> Text -> IO ()
updateAgentStatus conn runId status = do
@@ -619,21 +633,22 @@ updateAgentStatus conn runId status = do
conn
"UPDATE agents SET status = ? WHERE run_id = ?"
(status, runId)
+ touchUpdated conn runId
updateAgentStarted :: SQL.Connection -> Text -> Time.UTCTime -> IO ()
updateAgentStarted conn runId startTime = do
SQL.execute
conn
- "UPDATE agents SET status = 'running', started_at = ? WHERE run_id = ?"
- (tshow startTime, runId)
+ "UPDATE agents SET status = 'running', started_at = ?, updated_at = ? WHERE run_id = ?"
+ (tshow startTime, tshow startTime, runId)
updateAgentCompleted :: SQL.Connection -> Text -> Time.UTCTime -> Maybe Text -> Maybe Text -> Maybe Int -> IO ()
updateAgentCompleted conn runId endTime mError mSummary mCost = do
let status = maybe "completed" (const "failed") mError
SQL.execute
conn
- "UPDATE agents SET status = ?, completed_at = ?, error = ?, summary = ?, cost_cents = ? WHERE run_id = ?"
- (status :: Text, tshow endTime, mError, mSummary, mCost, runId)
+ "UPDATE agents SET status = ?, completed_at = ?, updated_at = ?, error = ?, summary = ?, cost_cents = ? WHERE run_id = ?"
+ (status :: Text, tshow endTime, tshow endTime, mError, mSummary, mCost, runId)
updateAgentIdle :: SQL.Connection -> Text -> Maybe Text -> Maybe Int -> IO ()
updateAgentIdle conn runId mSummary mCost = do
@@ -641,6 +656,7 @@ updateAgentIdle conn runId mSummary mCost = do
conn
"UPDATE agents SET status = 'idle', summary = ?, cost_cents = ? WHERE run_id = ?"
(mSummary, mCost, runId)
+ touchUpdated conn runId
updateAgentSummary :: SQL.Connection -> Text -> Maybe Text -> Maybe Int -> IO ()
updateAgentSummary conn runId mSummary mCost = do
@@ -648,6 +664,7 @@ updateAgentSummary conn runId mSummary mCost = do
conn
"UPDATE agents SET summary = ?, cost_cents = ? WHERE run_id = ?"
(mSummary, mCost, runId)
+ touchUpdated conn runId
updateAgentTitle :: SQL.Connection -> Text -> Text -> IO ()
updateAgentTitle conn runId title = do
@@ -655,13 +672,14 @@ updateAgentTitle conn runId title = do
conn
"UPDATE agents SET title = ? WHERE run_id = ?"
(title, runId)
+ touchUpdated conn runId
updateAgentCompletedAt :: SQL.Connection -> Text -> Time.UTCTime -> IO ()
updateAgentCompletedAt conn runId endTime = do
SQL.execute
conn
- "UPDATE agents SET status = 'completed', completed_at = ? WHERE run_id = ?"
- (tshow endTime, runId)
+ "UPDATE agents SET status = 'completed', completed_at = ?, updated_at = ? WHERE run_id = ?"
+ (tshow endTime, tshow endTime, runId)
insertWorkspace ::
SQL.Connection ->
@@ -790,7 +808,7 @@ getAgent conn runId = do
results <-
SQL.query @_ @AgentInfo
conn
- "SELECT run_id, status, prompt, workspace, pid, started_at, completed_at, error, summary, cost_cents, title FROM agents WHERE run_id = ?"
+ "SELECT run_id, status, prompt, workspace, pid, started_at, updated_at, completed_at, error, summary, cost_cents, title FROM agents WHERE run_id = ?"
(SQL.Only runId)
case results of
[] -> pure Nothing
@@ -805,6 +823,7 @@ instance SQL.FromRow AgentInfo where
<*> SQL.field -- workspace
<*> SQL.field -- pid
<*> (parseTime </ SQL.field) -- started_at
+ <*> (parseTime </ SQL.field) -- updated_at
<*> (parseTime </ SQL.field) -- completed_at
<*> SQL.field -- error
<*> SQL.field -- summary
@@ -815,7 +834,7 @@ getAllAgents :: SQL.Connection -> IO [AgentInfo]
getAllAgents conn =
SQL.query_ @AgentInfo
conn
- "SELECT run_id, status, prompt, workspace, pid, started_at, completed_at, error, summary, cost_cents, title FROM agents ORDER BY created_at DESC"
+ "SELECT run_id, status, prompt, workspace, pid, started_at, updated_at, completed_at, error, summary, cost_cents, title FROM agents ORDER BY created_at DESC"
parseTime :: Maybe Text -> Maybe Time.UTCTime
parseTime Nothing = Nothing
@@ -902,6 +921,7 @@ defaultAgentInfo cfg =
aiWorkspace = Text.pack (acCwd cfg),
aiPid = Nothing,
aiStartedAt = Nothing,
+ aiUpdatedAt = Nothing,
aiCompletedAt = Nothing,
aiError = Nothing,
aiSummary = Nothing,
@@ -1051,12 +1071,13 @@ createAgent mDbPath cfg = do
Nothing -> do
_ <- writeAgentEnvFile normalizedCfg
_ <- runSystemctlUser ["daemon-reload"]
+ now <- Time.getCurrentTime
let workspaceText = Text.pack (acCwd normalizedCfg)
insertResult <-
try @SomeException
<| SQL.execute
conn
- "INSERT INTO agents (run_id, prompt, workspace, status, provider, model, cwd, thinking, extra_args, extra_env) VALUES (?, '', ?, 'stopped', ?, ?, ?, ?, ?, ?)"
+ "INSERT INTO agents (run_id, prompt, workspace, status, provider, model, cwd, thinking, extra_args, extra_env, updated_at) VALUES (?, '', ?, 'stopped', ?, ?, ?, ?, ?, ?, ?)"
( runId,
workspaceText,
acProvider normalizedCfg,
@@ -1064,7 +1085,8 @@ createAgent mDbPath cfg = do
workspaceText,
acThinking normalizedCfg,
acExtraArgs normalizedCfg,
- encodeExtraEnv (acExtraEnv normalizedCfg)
+ encodeExtraEnv (acExtraEnv normalizedCfg),
+ tshow now
)
case insertResult of
Left err -> pure (Left ("Failed to create agent: " <> tshow err))
@@ -1127,7 +1149,7 @@ stopPersistentAgent mDbPath runId =
Left err -> pure (Left err)
Right () -> do
now <- Time.getCurrentTime
- SQL.execute conn "UPDATE agents SET status = 'stopped', completed_at = ? WHERE run_id = ?" (tshow now, runId)
+ SQL.execute conn "UPDATE agents SET status = 'stopped', completed_at = ?, updated_at = ? WHERE run_id = ?" (tshow now, tshow now, runId)
Right </ hydratePersistentAgent conn cfg
restartPersistentAgent :: Maybe FilePath -> Text -> IO (Either Text PersistentAgent)
@@ -2031,6 +2053,7 @@ test =
aiWorkspace = "/home/test",
aiPid = Just 12345,
aiStartedAt = Nothing,
+ aiUpdatedAt = Nothing,
aiCompletedAt = Nothing,
aiError = Nothing,
aiSummary = Just "Test summary",
@@ -2039,7 +2062,9 @@ test =
}
case Aeson.decode (Aeson.encode info) :: Maybe Aeson.Value of
Nothing -> Test.assertFailure "Failed to serialize AgentInfo"
- Just _ -> pure (),
+ Just (Aeson.Object obj) ->
+ Test.assertBool "updated_at key should be present" (isJust (KeyMap.lookup "updated_at" obj))
+ Just _ -> Test.assertFailure "Expected object JSON for AgentInfo",
Test.group
"integration"
[ Test.unit "spawn completes and sends webhook" integrationSpawnWebhook,