Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/mesh/src/storage/monitoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,32 @@ export class SqlMonitoringStorage implements MonitoringStorage {
return sql`to_timestamp(floor(extract(epoch from timestamp::timestamp) / ${bucketSeconds}) * ${bucketSeconds})`;
}

async getLastUsedByVirtualMcpIds(
organizationId: string,
virtualMcpIds: string[],
): Promise<Record<string, string>> {
if (virtualMcpIds.length === 0) return {};

const rows = await this.db
.selectFrom("monitoring_logs")
.select([
"virtual_mcp_id",
(eb) => eb.fn.max("timestamp").as("last_used"),
])
.where("organization_id", "=", organizationId)
.where("virtual_mcp_id", "in", virtualMcpIds)
.groupBy("virtual_mcp_id")
.execute();

const result: Record<string, string> = {};
for (const row of rows) {
if (row.virtual_mcp_id && row.last_used) {
result[row.virtual_mcp_id] = String(row.last_used);
}
}
return result;
}

// ============================================================================
// Private Helper Methods
// ============================================================================
Expand Down
4 changes: 4 additions & 0 deletions apps/mesh/src/storage/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ export interface MonitoringStorage {
errorRate: number;
avgDurationMs: number;
}>;
getLastUsedByVirtualMcpIds(
organizationId: string,
virtualMcpIds: string[],
): Promise<Record<string, string>>;
}

// ============================================================================
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const CORE_TOOLS = [
// Monitoring tools
MonitoringTools.MONITORING_LOGS_LIST,
MonitoringTools.MONITORING_STATS,
MonitoringTools.MONITORING_AGENT_LAST_USED,

// Monitoring Dashboard tools
MonitoringDashboardTools.MONITORING_DASHBOARD_CREATE,
Expand Down
46 changes: 46 additions & 0 deletions apps/mesh/src/tools/monitoring/agent-last-used.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* MONITORING_AGENT_LAST_USED Tool
*
* Returns the last time each agent (Virtual MCP) was used,
* based on monitoring logs. Useful for identifying stale agents.
*/

import { requireOrganization } from "@/core/mesh-context";
import { defineTool } from "../../core/define-tool";
import { z } from "zod";

export const MONITORING_AGENT_LAST_USED = defineTool({
name: "MONITORING_AGENT_LAST_USED",
description:
"Get the last usage timestamp for each agent (Virtual MCP), derived from monitoring logs",
annotations: {
title: "Get Agent Last Used",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
inputSchema: z.object({
virtualMcpIds: z
.array(z.string())
.max(500)
.describe("List of Virtual MCP (Agent) IDs to check"),
}),
outputSchema: z.object({
lastUsed: z
.record(z.string(), z.string())
.describe(
"Map of virtualMcpId to last used ISO 8601 timestamp. Missing keys mean the agent was never used.",
),
}),
handler: async (input, ctx) => {
const org = requireOrganization(ctx);

const lastUsed = await ctx.storage.monitoring.getLastUsedByVirtualMcpIds(
org.id,
input.virtualMcpIds,
);

return { lastUsed };
},
});
1 change: 1 addition & 0 deletions apps/mesh/src/tools/monitoring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

export { MONITORING_LOGS_LIST } from "./list";
export { MONITORING_STATS } from "./stats";
export { MONITORING_AGENT_LAST_USED } from "./agent-last-used";
7 changes: 7 additions & 0 deletions apps/mesh/src/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const ALL_TOOL_NAMES = [
// Monitoring tools
"MONITORING_LOGS_LIST",
"MONITORING_STATS",
"MONITORING_AGENT_LAST_USED",
// Monitoring Dashboard tools
"MONITORING_DASHBOARD_CREATE",
"MONITORING_DASHBOARD_GET",
Expand Down Expand Up @@ -326,6 +327,11 @@ export const MANAGEMENT_TOOLS: ToolMetadata[] = [
description: "View monitoring statistics",
category: "Monitoring",
},
{
name: "MONITORING_AGENT_LAST_USED",
description: "Get agent last usage timestamps",
category: "Monitoring",
},
// Monitoring Dashboard tools
{
name: "MONITORING_DASHBOARD_CREATE",
Expand Down Expand Up @@ -595,6 +601,7 @@ const TOOL_LABELS: Record<ToolName, string> = {
COLLECTION_VIRTUAL_TOOLS_DELETE: "Delete virtual tools",
MONITORING_LOGS_LIST: "List monitoring logs",
MONITORING_STATS: "View monitoring statistics",
MONITORING_AGENT_LAST_USED: "Get agent last usage timestamps",
MONITORING_DASHBOARD_CREATE: "Create monitoring dashboards",
MONITORING_DASHBOARD_GET: "View dashboard details",
MONITORING_DASHBOARD_LIST: "List monitoring dashboards",
Expand Down
35 changes: 28 additions & 7 deletions apps/mesh/src/web/components/home/agents-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
} from "@deco/ui/components/popover.tsx";
import { Skeleton } from "@deco/ui/components/skeleton.tsx";
import { cn } from "@deco/ui/lib/utils.ts";
import { isDecopilot, useVirtualMCPs } from "@decocms/mesh-sdk";
import {
isDecopilot,
useVirtualMCPs,
useAgentLastUsed,
} from "@decocms/mesh-sdk";
import { ChevronRight, Users03 } from "@untitledui/icons";
import { Suspense, useEffect, useRef, useState } from "react";

Expand Down Expand Up @@ -140,12 +144,29 @@ function AgentsListContent() {
const virtualMcps = useVirtualMCPs();
const { selectedVirtualMcp, setVirtualMcpId } = useChatStable();

// Filter out the default Decopilot agent (it's not a real agent)
const agents = virtualMcps
.filter(
(agent): agent is typeof agent & { id: string } =>
agent.id !== null && !isDecopilot(agent.id),
)
const nonDecopilotAgents = virtualMcps.filter(
(agent): agent is typeof agent & { id: string } =>
agent.id !== null && !isDecopilot(agent.id),
);

const agentIds = nonDecopilotAgents.map((a) => a.id);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Cap the number of agent IDs sent to useAgentLastUsed (the tool rejects more than 500). Otherwise orgs with >500 agents will hit validation errors and this query will fail.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/home/agents-list.tsx, line 152:

<comment>Cap the number of agent IDs sent to useAgentLastUsed (the tool rejects more than 500). Otherwise orgs with >500 agents will hit validation errors and this query will fail.</comment>

<file context>
@@ -140,12 +144,29 @@ function AgentsListContent() {
+      agent.id !== null && !isDecopilot(agent.id),
+  );
+
+  const agentIds = nonDecopilotAgents.map((a) => a.id);
+  const lastUsedMap = useAgentLastUsed(agentIds);
+
</file context>
Fix with Cubic

const lastUsedMap = useAgentLastUsed(agentIds);

// Sort by actual last usage (most recent first), then by updated_at as fallback
const agents = [...nonDecopilotAgents]
.sort((a, b) => {
const aUsed = lastUsedMap[a.id];
const bUsed = lastUsedMap[b.id];
if (aUsed && bUsed)
return new Date(bUsed).getTime() - new Date(aUsed).getTime();
if (aUsed && !bUsed) return -1;
if (!aUsed && bUsed) return 1;
const aUpdated = a.updated_at ?? a.created_at;
const bUpdated = b.updated_at ?? b.created_at;
if (aUpdated && bUpdated)
return new Date(bUpdated).getTime() - new Date(aUpdated).getTime();
return 0;
})
.slice(0, 6);

// Don't render if no agents
Expand Down
107 changes: 96 additions & 11 deletions apps/mesh/src/web/routes/orgs/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
useProjectContext,
useVirtualMCPs,
useVirtualMCPActions,
useAgentLastUsed,
type VirtualMCPEntity,
} from "@decocms/mesh-sdk";
import {
Expand Down Expand Up @@ -154,6 +155,31 @@ function getUniqueCreators(agents: VirtualMCPEntity[]): string[] {
// ---------------------------------------------------------------------------

type AgentStatusFilter = "ALL" | "active" | "inactive";
type AgentUsageFilter = "ALL" | "recent" | "stale" | "never";

const STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000; // 30 days

// ---------------------------------------------------------------------------
// Last used label
// ---------------------------------------------------------------------------

function AgentLastUsedLabel({ lastUsed }: { lastUsed: string | undefined }) {
return (
<div className="flex items-center justify-between w-full">
<span className="text-muted-foreground/70">Last used</span>
<span
className={cn(
"shrink-0",
lastUsed
? "text-muted-foreground"
: "text-muted-foreground/50 italic",
)}
>
{lastUsed ? formatTimeAgo(new Date(lastUsed)) : "Never"}
</span>
</div>
);
}

// ---------------------------------------------------------------------------
// Dialog state
Expand Down Expand Up @@ -759,6 +785,11 @@ function OrgAgentsContent() {

// Status filter
const [statusFilter, setStatusFilter] = useState<AgentStatusFilter>("ALL");
const [usageFilter, setUsageFilter] = useState<AgentUsageFilter>("ALL");

// Fetch last-used timestamps for all agents
const agentIds = virtualMcps.map((a) => a.id).filter(Boolean);
const lastUsedMap = useAgentLastUsed(agentIds);

const toggleSelect = (id: string) => {
setSelectedIds((prev) => {
Expand All @@ -777,17 +808,33 @@ function OrgAgentsContent() {
// Filtered agents
const filteredAgents = virtualMcps.filter((a) => {
if (statusFilter !== "ALL" && a.status !== statusFilter) return false;
if (usageFilter !== "ALL") {
const lastUsed = lastUsedMap[a.id];
if (usageFilter === "never" && lastUsed) return false;
if (usageFilter === "stale") {
if (!lastUsed) return false;
const age = Date.now() - new Date(lastUsed).getTime();
if (age < STALE_THRESHOLD_MS) return false;
}
if (usageFilter === "recent") {
if (!lastUsed) return false;
const age = Date.now() - new Date(lastUsed).getTime();
if (age >= STALE_THRESHOLD_MS) return false;
}
}
return true;
});

// Grouped items
const grouped = groupAgents(filteredAgents);

// Stats
const neverUsedCount = virtualMcps.filter((a) => !lastUsedMap[a.id]).length;
const stats = {
total: virtualMcps.length,
active: virtualMcps.filter((a) => a.status === "active").length,
inactive: virtualMcps.filter((a) => a.status === "inactive").length,
neverUsed: neverUsedCount,
};

// Delete handlers
Expand Down Expand Up @@ -912,6 +959,26 @@ function OrgAgentsContent() {
cellClassName: "w-32 shrink-0",
sortable: true,
},
{
id: "last_used",
header: "Last used",
render: (virtualMcp) => {
const ts = lastUsedMap[virtualMcp.id];
if (!ts) {
return (
<span className="text-xs whitespace-nowrap text-muted-foreground/50 italic">
Never
</span>
);
}
return (
<span className="text-xs whitespace-nowrap text-muted-foreground">
{formatTimeAgo(new Date(ts))}
</span>
);
},
cellClassName: "max-w-24 w-24 shrink-0",
},
{
id: "updated_at",
header: "Updated",
Expand Down Expand Up @@ -1073,6 +1140,7 @@ function OrgAgentsContent() {
{stats.total} total
{stats.active > 0 && ` · ${stats.active} active`}
{stats.inactive > 0 && ` · ${stats.inactive} inactive`}
{stats.neverUsed > 0 && ` · ${stats.neverUsed} never used`}
</span>
</BreadcrumbPage>
</BreadcrumbItem>
Expand Down Expand Up @@ -1104,6 +1172,18 @@ function OrgAgentsContent() {
{ id: "inactive", label: "Inactive" },
],
},
{
label: "Usage",
value: usageFilter,
onChange: (v) =>
setUsageFilter((v as AgentUsageFilter) || "ALL"),
options: [
{ id: "ALL", label: "All" },
{ id: "recent", label: "Recently used" },
{ id: "stale", label: "Stale (30+ days)" },
{ id: "never", label: "Never used" },
],
},
]}
/>
{ctaButton}
Expand Down Expand Up @@ -1188,18 +1268,23 @@ function OrgAgentsContent() {
)}
body={<ConnectionStatus status={agent.status} />}
footer={
<div className="flex items-center justify-between text-xs text-muted-foreground w-full min-w-0">
<div className="flex-1 min-w-0">
<User
id={agent.updated_by ?? agent.created_by}
size="3xs"
/>
<div className="flex flex-col gap-1 text-xs text-muted-foreground w-full min-w-0">
<div className="flex items-center justify-between w-full min-w-0">
<div className="flex-1 min-w-0">
<User
id={agent.updated_by ?? agent.created_by}
size="3xs"
/>
</div>
<span className="shrink-0 ml-2">
{agent.updated_at
? formatTimeAgo(new Date(agent.updated_at))
: "—"}
</span>
</div>
<span className="shrink-0 ml-2">
{agent.updated_at
? formatTimeAgo(new Date(agent.updated_at))
: "—"}
</span>
<AgentLastUsedLabel
lastUsed={lastUsedMap[agent.id]}
/>
</div>
}
headerActions={
Expand Down
1 change: 1 addition & 0 deletions packages/mesh-sdk/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export {
useVirtualMCPs,
useVirtualMCP,
useVirtualMCPActions,
useAgentLastUsed,
type VirtualMCPFilter,
type UseVirtualMCPsOptions,
} from "./use-virtual-mcp";
Loading