diff --git a/backend/Controllers/DashboardController.cs b/backend/Controllers/DashboardController.cs
index 14b7eb1..376daf4 100644
--- a/backend/Controllers/DashboardController.cs
+++ b/backend/Controllers/DashboardController.cs
@@ -52,36 +52,30 @@ public class DashboardController(
}
///
- /// Returns the latest assistant messages (operations/feed) from the Iris session.
- /// Filtered to role == "assistant" — those are the work feed entries.
+ /// Returns the latest assistant messages aggregated from ALL agent sessions.
+ /// Events are sorted by timestamp descending (newest first).
+ /// Supports optional agent filter via ?agent= query parameter.
+ /// Falls back to Iris-only feed if multi-agent feed fails.
///
[HttpGet("operations")]
- public async Task> GetOperations([FromQuery] int limit = 20)
+ public async Task> GetOperations(
+ [FromQuery] int limit = 20,
+ [FromQuery] string? agent = null)
{
try
{
- var messages = await gateway.GetSessionHistoryAsync("iris", Math.Clamp(limit, 1, 100));
- var feed = new List();
+ var entries = await gateway.GetAllAgentOperationsAsync(Math.Clamp(limit, 1, 100));
- foreach (var msg in messages)
+ // Optional agent filter
+ if (!string.IsNullOrWhiteSpace(agent))
{
- if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase))
- continue;
-
- if (string.IsNullOrWhiteSpace(msg.Content))
- continue;
-
- // Parse timestamp for display-friendly "time ago"
- var ts = ParseTimestamp(msg.Timestamp);
- var timeAgo = FormatTimeAgo(ts);
-
- // Extract a short agent indicator and action from content
- var (agent, action) = ExtractAgentAction(msg.Content);
-
- feed.Add(new FeedEntry(agent, action, msg.Timestamp, timeAgo));
+ entries = entries
+ .Where(e => string.Equals(e.AgentId, agent, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(e.Agent, agent, StringComparison.OrdinalIgnoreCase))
+ .ToList();
}
- return feed;
+ return entries;
}
catch (Exception ex)
{
@@ -367,55 +361,5 @@ public class DashboardController(
t.UpdatedAt
);
- private static DateTimeOffset ParseTimestamp(string timestamp)
- {
- if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt))
- return dt;
- return DateTimeOffset.UtcNow;
- }
- private static string FormatTimeAgo(DateTimeOffset ts)
- {
- var diff = DateTimeOffset.UtcNow - ts;
- if (diff.TotalMinutes < 1) return "just now";
- if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
- if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
- if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
- return ts.ToString("MMM dd");
- }
-
- private static (string Agent, string Action) ExtractAgentAction(string content)
- {
- // Take first line or first ~80 chars as the action summary
- var firstLine = content.Split('\n', 2)[0].Trim();
- var summary = firstLine.Length > 80 ? firstLine[..80] + "…" : firstLine;
-
- // Try to identify which agent this came from
- var agent = "Iris";
- foreach (var marker in new[] { "**Agent:**", "**Agent:** ", "*Agent:* ", "Agent:" })
- {
- var idx = content.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
- if (idx >= 0)
- {
- var after = content[(idx + marker.Length)..].TrimStart();
- var end = after.IndexOfAny(['\n', '\r', ',', '.']);
- var found = end > 0 ? after[..end].Trim() : after.Split('\n', 2)[0].Trim();
- if (!string.IsNullOrWhiteSpace(found) && found.Length < 30)
- {
- agent = found;
- break;
- }
- }
- }
-
- // Try to find agent name at the start in brackets like [Agent: Iris]
- if (agent == "Iris")
- {
- var bracketMatch = System.Text.RegularExpressions.Regex.Match(content, @"\[Agent:\s*([^\]]+)\]");
- if (bracketMatch.Success)
- agent = bracketMatch.Groups[1].Value.Trim();
- }
-
- return (agent, summary);
- }
}
diff --git a/backend/Models/Dashboard.cs b/backend/Models/Dashboard.cs
index 2b16484..6663e06 100644
--- a/backend/Models/Dashboard.cs
+++ b/backend/Models/Dashboard.cs
@@ -32,7 +32,9 @@ public sealed record FeedEntry(
string Agent,
string Action,
string Timestamp,
- string Time
+ string Time,
+ string? AgentId = null,
+ string? Type = null
);
public sealed record DashboardStatus(
diff --git a/backend/Services/OpenClawGatewayClient.cs b/backend/Services/OpenClawGatewayClient.cs
index 453dd35..98c432c 100644
--- a/backend/Services/OpenClawGatewayClient.cs
+++ b/backend/Services/OpenClawGatewayClient.cs
@@ -459,6 +459,138 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
return result;
}
+ ///
+ /// Collects assistant messages from ALL agent sessions (multi-agent operations feed).
+ /// Merges, sorts by timestamp descending, and limits the result.
+ /// Falls back to an empty list if any agent session is unreachable.
+ ///
+ public async Task> GetAllAgentOperationsAsync(int limit = 30)
+ {
+ var allEntries = new List();
+ var agentIds = LoadAgentIdsFromConfig();
+
+ foreach (var agentId in agentIds)
+ {
+ try
+ {
+ var sessionKey = $"agent:{agentId}:main";
+ var messages = await GetSessionHistoryAsync(sessionKey, Math.Min(limit * 2, 50));
+ foreach (var msg in messages)
+ {
+ if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ if (string.IsNullOrWhiteSpace(msg.Content))
+ continue;
+
+ // Parse timestamp
+ var ts = ParseTimestamp(msg.Timestamp);
+ var timeAgo = FormatTimeAgo(ts);
+
+ // Extract a short agent indicator and action from content
+ var (agent, action) = ExtractAgentAction(msg.Content);
+
+ // Determine event type based on content heuristics
+ var eventType = DetectEventType(msg.Content);
+
+ allEntries.Add(new FeedEntry(
+ agent,
+ action,
+ msg.Timestamp,
+ timeAgo,
+ AgentId: agentId,
+ Type: eventType
+ ));
+ }
+ }
+ catch
+ {
+ // Agent session unreachable — skip; we still have data from other agents
+ }
+ }
+
+ // Sort descending by timestamp, then limit
+ return allEntries
+ .OrderByDescending(e => ParseTimestamp(e.Timestamp))
+ .Take(Math.Clamp(limit, 1, 100))
+ .ToList();
+ }
+
+ private static DateTimeOffset ParseTimestamp(string timestamp)
+ {
+ if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt))
+ return dt;
+ return DateTimeOffset.UtcNow;
+ }
+
+ private static string FormatTimeAgo(DateTimeOffset ts)
+ {
+ var diff = DateTimeOffset.UtcNow - ts;
+ if (diff.TotalMinutes < 1) return "just now";
+ if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
+ if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
+ if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
+ return ts.ToString("MMM dd");
+ }
+
+ ///
+ /// Determines a FeedEntry event type from message content heuristics.
+ ///
+ private static string DetectEventType(string content)
+ {
+ if (content.Contains("Subagent Task") || content.Contains("subagent"))
+ {
+ if (content.Contains("complete") || content.Contains("done") || content.Contains("finished"))
+ return "task_complete";
+ return "task_start";
+ }
+ if (content.Contains("Deploy") || content.Contains("deploy") || content.Contains("publish"))
+ return "deploy";
+ if (content.Contains("System") || content.Contains("system") || content.Contains("health"))
+ return "system";
+ if (content.Contains("Gestartet") || content.Contains("started") || content.Contains("Session"))
+ return "session_start";
+ return "chat";
+ }
+
+ ///
+ /// Extracts a human-readable agent name and action summary from message content.
+ ///
+ private static (string Agent, string Action) ExtractAgentAction(string content)
+ {
+ // Take first line or first ~80 chars as the action summary
+ var firstLine = content.Split('\n', 2)[0].Trim();
+ var summary = firstLine.Length > 80 ? firstLine[..80] + "\u2026" : firstLine;
+
+ // Try to identify which agent this came from
+ var agent = "Iris";
+ foreach (var marker in new[] { "**Agent:**", "**Agent:** ", "*Agent:* ", "Agent:" })
+ {
+ var idx = content.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
+ if (idx >= 0)
+ {
+ var after = content[(idx + marker.Length)..].TrimStart();
+ var end = after.IndexOfAny(['\n', '\r', ',', '.']);
+ var found = end > 0 ? after[..end].Trim() : after.Split('\n', 2)[0].Trim();
+ if (!string.IsNullOrWhiteSpace(found) && found.Length < 30)
+ {
+ agent = found;
+ break;
+ }
+ }
+ }
+
+ // Try to find agent name at the start in brackets like [Agent: Iris]
+ if (agent == "Iris")
+ {
+ var bracketMatch = System.Text.RegularExpressions.Regex.Match(content, @"\[Agent:\s*([^\]]+)\]");
+ if (bracketMatch.Success)
+ agent = bracketMatch.Groups[1].Value.Trim();
+ }
+
+ return (agent, summary);
+ }
+
public async Task SendChatMessageAsync(string agentId, string message)
{
try